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,55 @@
1
+ """Cognitive Kernel (the OS layer for Velune)."""
2
+
3
+ from velune.events import CognitiveBus, Event, EventBus, EventHandler, Subscription
4
+ from velune.kernel.config import (
5
+ ConfigLoader,
6
+ ConfigService,
7
+ ConfigValidationError,
8
+ ContextConfig,
9
+ ExecutionConfig,
10
+ MemoryConfig,
11
+ ProjectConfig,
12
+ ProvidersConfig,
13
+ RetrievalConfig,
14
+ VeluneConfig,
15
+ WorkspaceConfig,
16
+ get_default_config,
17
+ )
18
+ from velune.kernel.health import SubsystemHealthMonitor
19
+ from velune.kernel.lifecycle import LifecycleCoordinator, Subsystem
20
+ from velune.kernel.registry import ServiceContainer, get_container, inject
21
+ from velune.kernel.schemas import ComponentStatus, HealthReport
22
+
23
+ __all__ = [
24
+ # Schemas
25
+ "ComponentStatus",
26
+ "Event",
27
+ "HealthReport",
28
+ # Bus
29
+ "CognitiveBus",
30
+ "Subscription",
31
+ "EventBus",
32
+ "EventHandler",
33
+ # Registry
34
+ "ServiceContainer",
35
+ "inject",
36
+ "get_container",
37
+ # Lifecycle
38
+ "LifecycleCoordinator",
39
+ "Subsystem",
40
+ # Config
41
+ "VeluneConfig",
42
+ "ConfigValidationError",
43
+ "ProjectConfig",
44
+ "WorkspaceConfig",
45
+ "ContextConfig",
46
+ "MemoryConfig",
47
+ "RetrievalConfig",
48
+ "ExecutionConfig",
49
+ "ProvidersConfig",
50
+ "ConfigLoader",
51
+ "ConfigService",
52
+ "get_default_config",
53
+ # Health
54
+ "SubsystemHealthMonitor",
55
+ ]
@@ -0,0 +1,125 @@
1
+ import logging
2
+ from collections.abc import Callable
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from velune.kernel.config import VeluneConfig
8
+ from velune.kernel.lifecycle import LifecycleCoordinator
9
+ from velune.kernel.registry import ServiceContainer
10
+
11
+ _logger = logging.getLogger(__name__)
12
+
13
+
14
+ @dataclass
15
+ class SubsystemModule:
16
+ """Declares a subsystem's factory and dependencies."""
17
+
18
+ name: str
19
+ factory: Callable[["RuntimeEnvironment"], Any]
20
+ container_key: str
21
+ lifecycle_key: str | None = None # None = not lifecycle-managed
22
+ dependencies: list[str] = field(default_factory=list) # container keys this needs
23
+
24
+
25
+ @dataclass
26
+ class RuntimeEnvironment:
27
+ """Everything a subsystem factory needs to initialize itself."""
28
+
29
+ workspace: Path
30
+ config: VeluneConfig
31
+ container: ServiceContainer
32
+ lifecycle: LifecycleCoordinator
33
+ verbose: bool = False
34
+
35
+
36
+ class RuntimeBootstrapper:
37
+ def __init__(self) -> None:
38
+ self._modules: list[SubsystemModule] = []
39
+
40
+ def register_module(self, module: SubsystemModule) -> None:
41
+ self._modules.append(module)
42
+
43
+ def bootstrap(self, env: RuntimeEnvironment) -> None:
44
+ """Initialize all modules in dependency order."""
45
+ from velune.core.startup_profiler import mark
46
+ from velune.core.task_registry import BackgroundTaskRegistry
47
+
48
+ registry = BackgroundTaskRegistry()
49
+ env.container.register_instance("runtime.task_registry", registry)
50
+
51
+ from velune.hardware.detector import HardwareDetector
52
+
53
+ hardware_profile = HardwareDetector().detect()
54
+ env.container.register_instance("runtime.hardware", hardware_profile)
55
+ mark("hardware detected")
56
+
57
+ # Modules with lifecycle_key set are lifecycle-critical (startup/shutdown
58
+ # managed by LifecycleCoordinator). A factory failure aborts bootstrap.
59
+ # Modules with lifecycle_key=None are optional; a factory failure is
60
+ # logged and that module is skipped, letting the rest of the system start.
61
+ resolved = self._topological_sort()
62
+ for module in resolved:
63
+ try:
64
+ instance = module.factory(env)
65
+ mark(f"module: {module.name}")
66
+ except Exception as exc:
67
+ if module.lifecycle_key:
68
+ _logger.critical(
69
+ "Critical module '%s' (%s) failed to initialize: %s",
70
+ module.name,
71
+ module.container_key,
72
+ exc,
73
+ )
74
+ raise
75
+ _logger.warning(
76
+ "Optional module '%s' (%s) failed to initialize, skipping: %s",
77
+ module.name,
78
+ module.container_key,
79
+ exc,
80
+ )
81
+ continue
82
+ env.container.register_instance(module.container_key, instance)
83
+ if module.lifecycle_key:
84
+ env.lifecycle.register(module.lifecycle_key, instance)
85
+
86
+ def _topological_sort(self) -> list[SubsystemModule]:
87
+ # Kahn's algorithm on dependency graph
88
+ in_degree = {}
89
+ graph = {}
90
+ key_to_mod = {mod.container_key: mod for mod in self._modules}
91
+
92
+ for mod in self._modules:
93
+ in_degree[mod.container_key] = 0
94
+ graph[mod.container_key] = []
95
+
96
+ for mod in self._modules:
97
+ # only consider dependencies that are other modules
98
+ for dep in mod.dependencies:
99
+ if dep in key_to_mod:
100
+ graph[dep].append(mod.container_key)
101
+ in_degree[mod.container_key] += 1
102
+
103
+ # Find all modules with in_degree == 0
104
+ from collections import deque
105
+
106
+ queue = deque(
107
+ [mod.container_key for mod in self._modules if in_degree[mod.container_key] == 0]
108
+ )
109
+
110
+ resolved_keys = []
111
+ while queue:
112
+ node = queue.popleft()
113
+ resolved_keys.append(node)
114
+ for neighbor in graph[node]:
115
+ in_degree[neighbor] -= 1
116
+ if in_degree[neighbor] == 0:
117
+ queue.append(neighbor)
118
+
119
+ if len(resolved_keys) != len(self._modules):
120
+ unresolved = set(key_to_mod.keys()) - set(resolved_keys)
121
+ raise ValueError(
122
+ f"Circular dependency or missing module dependency detected in bootstrap! Unresolved: {unresolved}"
123
+ )
124
+
125
+ return [key_to_mod[key] for key in resolved_keys]
@@ -0,0 +1,426 @@
1
+ """Layered configuration engine with env overrides, schemas, and workspace discovery."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ import toml
10
+ from pydantic import BaseModel, Field
11
+ from pydantic_settings import BaseSettings, SettingsConfigDict
12
+
13
+
14
+ @dataclass
15
+ class ConfigValidationError:
16
+ """A single configuration validation problem."""
17
+
18
+ field: str
19
+ value: str | None
20
+ reason: str
21
+ severity: str = "CRITICAL" # "CRITICAL" or "WARNING"
22
+
23
+
24
+ class ProjectConfig(BaseModel):
25
+ """Project-level metadata."""
26
+
27
+ name: str = "velune"
28
+ version: str = "0.1.0"
29
+
30
+
31
+ class WorkspaceConfig(BaseModel):
32
+ """Workspace cognition settings."""
33
+
34
+ index_on_init: bool = True
35
+ watch_files: bool = True
36
+ git_aware: bool = True
37
+ root: Path | None = None
38
+
39
+
40
+ class ContextConfig(BaseModel):
41
+ """Context window budgeting settings."""
42
+
43
+ max_tokens: int = 128000
44
+ compression_threshold: float = Field(default=0.8, ge=0.0, le=1.0)
45
+ priority_tiers: list[str] = Field(default_factory=lambda: ["critical", "high", "medium", "low"])
46
+
47
+
48
+ class MemoryConfig(BaseModel):
49
+ """Memory retention and thresholds."""
50
+
51
+ working_memory_ttl: int = 3600 # seconds
52
+ episodic_retention_days: int = 30
53
+ semantic_threshold: float = Field(default=0.85, ge=0.0, le=1.0)
54
+ graph_enabled: bool = True
55
+ storage_dir: Path | None = None
56
+
57
+
58
+ class RetrievalConfig(BaseModel):
59
+ """Hybrid retrieval fusion weightings."""
60
+
61
+ vector_weight: float = Field(default=0.5, ge=0.0, le=1.0)
62
+ lexical_weight: float = Field(default=0.3, ge=0.0, le=1.0)
63
+ graph_weight: float = Field(default=0.2, ge=0.0, le=1.0)
64
+ rerank_top_k: int = Field(default=10, ge=1)
65
+
66
+
67
+ class ExecutionConfig(BaseModel):
68
+ """Safety and sandboxing options."""
69
+
70
+ sandbox_enabled: bool = True
71
+ auto_snapshot: bool = True
72
+ require_confirmation: bool = True
73
+ dry_run_default: bool = False
74
+ low_resource_mode: bool = False
75
+ allowed_executables: list[str] = Field(
76
+ default_factory=lambda: [
77
+ "python",
78
+ "python3",
79
+ "pytest",
80
+ "ruff",
81
+ "mypy",
82
+ "git",
83
+ "node",
84
+ "npm",
85
+ "cargo",
86
+ "go",
87
+ "make",
88
+ "cmake",
89
+ "gcc",
90
+ "clang",
91
+ "echo",
92
+ "cat",
93
+ "ls",
94
+ "find",
95
+ "grep",
96
+ ]
97
+ )
98
+
99
+
100
+ class ProviderEntry(BaseModel):
101
+ """Target address and API key names for LLM providers."""
102
+
103
+ api_key_env: str | None = None
104
+ base_url: str | None = None
105
+
106
+
107
+ class ProvidersConfig(BaseModel):
108
+ """Configuration for active and fallback models."""
109
+
110
+ default_provider: str = "openai"
111
+ fallback_providers: list[str] = Field(default_factory=lambda: ["anthropic", "ollama"])
112
+ cost_threshold_usd: float = Field(
113
+ default=0.01,
114
+ description="Prompt for confirmation before cloud calls estimated to cost more than this (USD). Set to 0 to always ask.",
115
+ )
116
+ openai: ProviderEntry | None = Field(
117
+ default_factory=lambda: ProviderEntry(
118
+ api_key_env="OPENAI_API_KEY", base_url="https://api.openai.com/v1"
119
+ )
120
+ )
121
+ anthropic: ProviderEntry | None = Field(
122
+ default_factory=lambda: ProviderEntry(
123
+ api_key_env="ANTHROPIC_API_KEY", base_url="https://api.anthropic.com"
124
+ )
125
+ )
126
+ ollama: ProviderEntry | None = Field(
127
+ default_factory=lambda: ProviderEntry(base_url="http://localhost:11434")
128
+ )
129
+ lmstudio: ProviderEntry | None = Field(
130
+ default_factory=lambda: ProviderEntry(base_url="http://localhost:1234/v1")
131
+ )
132
+ llamacpp: ProviderEntry | None = Field(default_factory=lambda: ProviderEntry(base_url=""))
133
+ huggingface: ProviderEntry | None = Field(
134
+ default_factory=lambda: ProviderEntry(
135
+ api_key_env="HF_TOKEN", base_url="https://api-inference.huggingface.co"
136
+ )
137
+ )
138
+
139
+
140
+ class TelemetryConfig(BaseModel):
141
+ """Observability options."""
142
+
143
+ enabled: bool = True
144
+ export_otlp: bool = False
145
+ log_level: str = "INFO"
146
+
147
+
148
+ class MCPConfig(BaseModel):
149
+ """MCP configuration settings."""
150
+
151
+ servers: dict[str, str] = Field(default_factory=dict)
152
+
153
+
154
+ class CognitionConfig(BaseModel):
155
+ """Cognitive routing and agent council settings."""
156
+
157
+ max_council_tier: str = "full" # instant, minimal, standard, full
158
+ default_tier_override: str = "auto" # auto, instant, minimal, standard, full
159
+
160
+
161
+ class VeluneConfig(BaseSettings):
162
+ """Root configuration tree.
163
+
164
+ Field values are resolved in this priority order (highest first):
165
+ 1. Explicit constructor kwargs (e.g. from TOML file data)
166
+ 2. Environment variables prefixed with ``VELUNE_`` (nested via ``__``)
167
+ 3. A ``.env`` file in the working directory
168
+ 4. Hard-coded field defaults
169
+ """
170
+
171
+ model_config = SettingsConfigDict(
172
+ env_prefix="VELUNE_",
173
+ env_file=".env",
174
+ env_file_encoding="utf-8",
175
+ env_nested_delimiter="__",
176
+ extra="ignore",
177
+ env_ignore_empty=True,
178
+ )
179
+
180
+ project: ProjectConfig = Field(default_factory=ProjectConfig)
181
+ workspace: WorkspaceConfig = Field(default_factory=WorkspaceConfig)
182
+ context: ContextConfig = Field(default_factory=ContextConfig)
183
+ memory: MemoryConfig = Field(default_factory=MemoryConfig)
184
+ retrieval: RetrievalConfig = Field(default_factory=RetrievalConfig)
185
+ execution: ExecutionConfig = Field(default_factory=ExecutionConfig)
186
+ providers: ProvidersConfig = Field(default_factory=ProvidersConfig)
187
+ telemetry: TelemetryConfig = Field(default_factory=TelemetryConfig)
188
+ mcp: MCPConfig = Field(default_factory=MCPConfig)
189
+ cognition: CognitionConfig = Field(default_factory=CognitionConfig)
190
+
191
+ # ---------------------------------------------------------------------------
192
+ # Startup validation
193
+ # ---------------------------------------------------------------------------
194
+
195
+ def validate(self) -> list[ConfigValidationError]:
196
+ """Validate the configuration and return any problems found.
197
+
198
+ Returns a list of :class:`ConfigValidationError`. An empty list means
199
+ the configuration is healthy. Callers should treat ``severity="CRITICAL"``
200
+ errors as startup-blocking failures.
201
+ """
202
+ errors: list[ConfigValidationError] = []
203
+
204
+ provider_name = self.providers.default_provider
205
+ provider_entry: ProviderEntry | None = getattr(self.providers, provider_name, None)
206
+
207
+ if not isinstance(provider_entry, ProviderEntry):
208
+ errors.append(
209
+ ConfigValidationError(
210
+ field="providers.default_provider",
211
+ value=provider_name,
212
+ reason=(
213
+ f"Provider '{provider_name}' is not defined in the [providers] section. "
214
+ f"Add a [{provider_name}] entry or change default_provider."
215
+ ),
216
+ severity="CRITICAL",
217
+ )
218
+ )
219
+ else:
220
+ # Remote providers require an API key env var to be populated.
221
+ if provider_entry.api_key_env:
222
+ resolved = os.getenv(provider_entry.api_key_env)
223
+ if not resolved:
224
+ errors.append(
225
+ ConfigValidationError(
226
+ field=f"providers.{provider_name}.api_key_env",
227
+ value=provider_entry.api_key_env,
228
+ reason=(
229
+ f"Environment variable '{provider_entry.api_key_env}' is not set. "
230
+ f"Provider '{provider_name}' requires an API key to function."
231
+ ),
232
+ severity="CRITICAL",
233
+ )
234
+ )
235
+
236
+ # Optional workspace root — validate if explicitly configured.
237
+ if self.workspace.root is not None:
238
+ wp = Path(self.workspace.root)
239
+ if not wp.exists():
240
+ errors.append(
241
+ ConfigValidationError(
242
+ field="workspace.root",
243
+ value=str(wp),
244
+ reason=f"Workspace path '{wp}' does not exist.",
245
+ severity="CRITICAL",
246
+ )
247
+ )
248
+ elif not os.access(wp, os.R_OK):
249
+ errors.append(
250
+ ConfigValidationError(
251
+ field="workspace.root",
252
+ value=str(wp),
253
+ reason=f"Workspace path '{wp}' exists but is not readable.",
254
+ severity="CRITICAL",
255
+ )
256
+ )
257
+
258
+ # Optional memory storage directory — validate writability if configured.
259
+ if self.memory.storage_dir is not None:
260
+ sd = Path(self.memory.storage_dir)
261
+ if not sd.exists():
262
+ errors.append(
263
+ ConfigValidationError(
264
+ field="memory.storage_dir",
265
+ value=str(sd),
266
+ reason=f"Storage directory '{sd}' does not exist.",
267
+ severity="WARNING",
268
+ )
269
+ )
270
+ elif not os.access(sd, os.W_OK):
271
+ errors.append(
272
+ ConfigValidationError(
273
+ field="memory.storage_dir",
274
+ value=str(sd),
275
+ reason=f"Storage directory '{sd}' exists but is not writable.",
276
+ severity="CRITICAL",
277
+ )
278
+ )
279
+
280
+ return errors
281
+
282
+ def save_to_project(self, workspace: Path) -> Path:
283
+ """Write only non-default values to .velune/config.toml.
284
+
285
+ Returns the path the file was written to.
286
+ """
287
+ project_config_dir = workspace / ".velune"
288
+ project_config_dir.mkdir(parents=True, exist_ok=True)
289
+ config_path = project_config_dir / "config.toml"
290
+
291
+ current_data = self.model_dump()
292
+ # Compare against hard-coded defaults, not an env-var-influenced instance.
293
+ default_data = _hardcoded_defaults()
294
+ non_default = _strip_defaults(current_data, default_data)
295
+
296
+ with open(config_path, "w", encoding="utf-8") as f:
297
+ toml.dump(non_default, f)
298
+
299
+ return config_path
300
+
301
+
302
+ # ---------------------------------------------------------------------------
303
+ # Internal helpers
304
+ # ---------------------------------------------------------------------------
305
+
306
+
307
+ def _hardcoded_defaults() -> dict:
308
+ """Return a pure-default VeluneConfig dict, unaffected by environment variables."""
309
+ instance = VeluneConfig.model_construct(
310
+ project=ProjectConfig(),
311
+ workspace=WorkspaceConfig(),
312
+ context=ContextConfig(),
313
+ memory=MemoryConfig(),
314
+ retrieval=RetrievalConfig(),
315
+ execution=ExecutionConfig(),
316
+ providers=ProvidersConfig(),
317
+ telemetry=TelemetryConfig(),
318
+ mcp=MCPConfig(),
319
+ cognition=CognitionConfig(),
320
+ )
321
+ return instance.model_dump()
322
+
323
+
324
+ def _deep_merge(base: dict, override: dict) -> dict:
325
+ """Recursively merge *override* into *base*, returning a new dict."""
326
+ result = base.copy()
327
+ for key, value in override.items():
328
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
329
+ result[key] = _deep_merge(result[key], value)
330
+ else:
331
+ result[key] = value
332
+ return result
333
+
334
+
335
+ def _strip_defaults(current: dict, defaults: dict) -> dict:
336
+ """Return only entries in *current* that differ from *defaults*."""
337
+ result: dict = {}
338
+ for key, value in current.items():
339
+ if key not in defaults:
340
+ result[key] = value
341
+ elif isinstance(value, dict) and isinstance(defaults[key], dict):
342
+ stripped = _strip_defaults(value, defaults[key])
343
+ if stripped:
344
+ result[key] = stripped
345
+ elif value != defaults[key]:
346
+ result[key] = value
347
+ return result
348
+
349
+
350
+ def get_default_config() -> VeluneConfig:
351
+ """Acquire standard default settings."""
352
+ return VeluneConfig()
353
+
354
+
355
+ class ConfigLoader:
356
+ """Loads and overlays configuration from TOML and Environment variables."""
357
+
358
+ def __init__(self, config_path: Path | None = None) -> None:
359
+ self.config_path = config_path or self._find_config_path()
360
+
361
+ def _find_config_path(self) -> Path | None:
362
+ """Traverse upwards to locate velune.toml, or fall back to user home."""
363
+ try:
364
+ current_dir = Path.cwd()
365
+ while current_dir != current_dir.parent:
366
+ config_file = current_dir / "velune.toml"
367
+ if config_file.exists():
368
+ return config_file
369
+ current_dir = current_dir.parent
370
+ except Exception:
371
+ pass
372
+
373
+ # Fallback to home directory config
374
+ home_config = Path.home() / ".velune" / "velune.toml"
375
+ if home_config.exists():
376
+ return home_config
377
+
378
+ return None
379
+
380
+ def load(self) -> VeluneConfig:
381
+ """Parse the TOML config file if it exists, otherwise return defaults.
382
+
383
+ Constructor kwargs take highest priority in BaseSettings, so TOML values
384
+ override env vars when an explicit config file is present. When no config
385
+ file is found, ``VeluneConfig()`` is returned and env vars apply normally.
386
+ """
387
+ if not self.config_path or not self.config_path.exists():
388
+ return get_default_config()
389
+
390
+ try:
391
+ data = toml.load(self.config_path)
392
+ return VeluneConfig(**data)
393
+ except Exception:
394
+ return get_default_config()
395
+
396
+ def load_with_env_overrides(self) -> VeluneConfig:
397
+ """Load config from the TOML file (if present), with env var fallback.
398
+
399
+ Since ``VeluneConfig`` is now a ``BaseSettings``, environment variables
400
+ prefixed with ``VELUNE_`` are always consulted for any field not supplied
401
+ by the TOML file.
402
+ """
403
+ return self.load()
404
+
405
+
406
+ @dataclass(slots=True)
407
+ class ConfigService:
408
+ """Workspace-aware configuration service."""
409
+
410
+ workspace: Path
411
+ config_path: Path | None = None
412
+
413
+ def load(self) -> VeluneConfig:
414
+ """Load configuration using workspace priorities."""
415
+ resolved = self._resolve_config_path()
416
+ return ConfigLoader(resolved).load_with_env_overrides()
417
+
418
+ def _resolve_config_path(self) -> Path | None:
419
+ if self.config_path:
420
+ return self.config_path
421
+
422
+ workspace_config = self.workspace / "velune.toml"
423
+ if workspace_config.exists():
424
+ return workspace_config
425
+
426
+ return None
@@ -0,0 +1,78 @@
1
+ """Single async runtime entry point for Velune.
2
+
3
+ ``asyncio.run`` is called **exactly once** in the entire codebase — inside
4
+ ``run_async()`` below. Every module that needs to bridge from sync to async
5
+ (CLI command callbacks, deprecated sync retrieval helpers, the daemon server)
6
+ imports and calls ``run_async()`` instead of calling ``asyncio.run``
7
+ directly.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import logging
14
+ from collections.abc import Coroutine
15
+ from typing import Any, TypeVar
16
+
17
+ _T = TypeVar("_T")
18
+ _logger = logging.getLogger(__name__)
19
+
20
+
21
+ def _install_uvloop() -> None:
22
+ """Swap the default asyncio event-loop policy for uvloop if available."""
23
+ try:
24
+ import uvloop # type: ignore[import-untyped]
25
+
26
+ asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
27
+ _logger.debug("uvloop event-loop policy installed.")
28
+ except ImportError:
29
+ pass
30
+
31
+
32
+ def run_async(coro: Coroutine[Any, Any, _T]) -> _T:
33
+ """Run *coro* to completion from a **synchronous** call site.
34
+
35
+ This is the **only** place in the entire Velune codebase that calls
36
+ ``asyncio.run``. All other callers must import and use this function.
37
+
38
+ Raises:
39
+ RuntimeError: If called from within a running event loop. Callers
40
+ inside an async context must ``await`` the coroutine directly.
41
+ """
42
+ try:
43
+ asyncio.get_running_loop()
44
+ except RuntimeError:
45
+ pass
46
+ else:
47
+ raise RuntimeError(
48
+ "run_async() called from a running event loop — await the coroutine directly instead."
49
+ )
50
+ _install_uvloop()
51
+ try:
52
+ return asyncio.run(coro)
53
+ except KeyboardInterrupt:
54
+ raise
55
+ except SystemExit:
56
+ raise
57
+
58
+
59
+ async def _async_main(runtime: Any) -> None:
60
+ """Top-level coroutine: run the interactive REPL session."""
61
+ from velune.cli.repl import VeluneREPL
62
+
63
+ repl = VeluneREPL(runtime)
64
+ await repl.run()
65
+
66
+
67
+ def launch(runtime: Any) -> None:
68
+ """Start the full interactive Velune session from a synchronous Typer callback.
69
+
70
+ Catches ``KeyboardInterrupt`` (Ctrl-C outside the REPL loop) so Typer sees
71
+ a clean exit. ``SystemExit`` is re-raised so ``typer.Exit`` works normally.
72
+ """
73
+ try:
74
+ run_async(_async_main(runtime))
75
+ except KeyboardInterrupt:
76
+ pass
77
+ except SystemExit:
78
+ raise