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.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +208 -0
- velune/cli/autocomplete.py +80 -0
- velune/cli/banner.py +60 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +175 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +228 -0
- velune/cli/commands/config.py +224 -0
- velune/cli/commands/daemon.py +88 -0
- velune/cli/commands/doctor.py +721 -0
- velune/cli/commands/init.py +170 -0
- velune/cli/commands/mcp.py +82 -0
- velune/cli/commands/memory.py +293 -0
- velune/cli/commands/models.py +683 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +270 -0
- velune/cli/commands/setup.py +184 -0
- velune/cli/commands/workspace.py +249 -0
- velune/cli/context.py +36 -0
- velune/cli/councilmodel_ui.py +199 -0
- velune/cli/display/council_view.py +254 -0
- velune/cli/display/memory_view.py +126 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +25 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +51 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +123 -0
- velune/cli/registry.py +80 -0
- velune/cli/rendering/__init__.py +5 -0
- velune/cli/rendering/error_panel.py +79 -0
- velune/cli/rendering/markdown.py +63 -0
- velune/cli/repl.py +1855 -0
- velune/cli/session_manager.py +71 -0
- velune/cli/slash_commands.py +37 -0
- velune/cli/theme.py +8 -0
- velune/cognition/__init__.py +23 -0
- velune/cognition/agents/__init__.py +7 -0
- velune/cognition/agents/coder.py +209 -0
- velune/cognition/agents/planner.py +156 -0
- velune/cognition/agents/reviewer.py +195 -0
- velune/cognition/arbitrator.py +220 -0
- velune/cognition/architecture.py +415 -0
- velune/cognition/budget.py +65 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +217 -0
- velune/cognition/council/challenger.py +74 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +43 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +46 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +56 -0
- velune/cognition/council/planner.py +124 -0
- velune/cognition/council/reviewer.py +74 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +188 -0
- velune/cognition/council_orchestrator.py +282 -0
- velune/cognition/firewall.py +354 -0
- velune/cognition/module.py +46 -0
- velune/cognition/orchestrator.py +1205 -0
- velune/cognition/personality.py +238 -0
- velune/cognition/state.py +104 -0
- velune/cognition/style_resolver.py +64 -0
- velune/cognition/verification.py +205 -0
- velune/context/__init__.py +28 -0
- velune/context/assembler.py +240 -0
- velune/context/budget.py +97 -0
- velune/context/extractive.py +95 -0
- velune/context/prompt_adaptation.py +480 -0
- velune/context/sections.py +99 -0
- velune/context/token_counter.py +134 -0
- velune/context/utilization.py +33 -0
- velune/context/window.py +63 -0
- velune/core/__init__.py +89 -0
- velune/core/background.py +5 -0
- velune/core/config/__init__.py +37 -0
- velune/core/errors/__init__.py +90 -0
- velune/core/errors/catalog.py +188 -0
- velune/core/errors/execution.py +31 -0
- velune/core/errors/memory.py +25 -0
- velune/core/errors/orchestration.py +31 -0
- velune/core/errors/provider.py +37 -0
- velune/core/event_loop.py +35 -0
- velune/core/logging.py +83 -0
- velune/core/paths.py +165 -0
- velune/core/runtime.py +113 -0
- velune/core/startup_profiler.py +56 -0
- velune/core/task_registry.py +117 -0
- velune/core/trace.py +83 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +53 -0
- velune/core/types/context.py +42 -0
- velune/core/types/inference.py +38 -0
- velune/core/types/memory.py +42 -0
- velune/core/types/model.py +70 -0
- velune/core/types/provider.py +62 -0
- velune/core/types/repository.py +38 -0
- velune/core/types/task.py +61 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +127 -0
- velune/daemon/transport.py +179 -0
- velune/events.py +204 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +315 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +130 -0
- velune/execution/command_spec.py +165 -0
- velune/execution/diff_preview.py +197 -0
- velune/execution/executor.py +181 -0
- velune/execution/module.py +18 -0
- velune/execution/multi_diff.py +67 -0
- velune/execution/path_guard.py +74 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +89 -0
- velune/execution/sandbox.py +268 -0
- velune/execution/validator.py +115 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +192 -0
- velune/kernel/__init__.py +55 -0
- velune/kernel/bootstrap.py +125 -0
- velune/kernel/config.py +426 -0
- velune/kernel/entrypoint.py +78 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +143 -0
- velune/kernel/module.py +17 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +96 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +115 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +624 -0
- velune/memory/__init__.py +32 -0
- velune/memory/compaction.py +506 -0
- velune/memory/embedding_pipeline.py +241 -0
- velune/memory/lifecycle.py +680 -0
- velune/memory/module.py +218 -0
- velune/memory/prioritizer.py +67 -0
- velune/memory/storage/episodic_schema.sql +53 -0
- velune/memory/storage/lancedb_store.py +282 -0
- velune/memory/storage/sqlite_manager.py +369 -0
- velune/memory/storage/sqlite_pool.py +149 -0
- velune/memory/tiers/episodic.py +588 -0
- velune/memory/tiers/graph.py +378 -0
- velune/memory/tiers/lineage.py +416 -0
- velune/memory/tiers/semantic.py +475 -0
- velune/memory/tiers/working.py +168 -0
- velune/memory/vitality.py +132 -0
- velune/models/__init__.py +15 -0
- velune/models/family.py +76 -0
- velune/models/module.py +20 -0
- velune/models/probes.py +192 -0
- velune/models/profile_cache.py +84 -0
- velune/models/profiler.py +108 -0
- velune/models/registry.py +251 -0
- velune/models/scorer.py +233 -0
- velune/models/specializations.py +205 -0
- velune/orchestration/__init__.py +19 -0
- velune/orchestration/engine.py +239 -0
- velune/orchestration/module.py +15 -0
- velune/orchestration/role_assignments.py +82 -0
- velune/orchestration/schemas.py +98 -0
- velune/plugins/__init__.py +20 -0
- velune/plugins/hooks.py +50 -0
- velune/plugins/loader.py +161 -0
- velune/plugins/registry.py +56 -0
- velune/plugins/schemas.py +21 -0
- velune/providers/__init__.py +23 -0
- velune/providers/adapters/anthropic.py +257 -0
- velune/providers/adapters/fireworks.py +115 -0
- velune/providers/adapters/google.py +234 -0
- velune/providers/adapters/groq.py +151 -0
- velune/providers/adapters/huggingface.py +210 -0
- velune/providers/adapters/llamacpp.py +208 -0
- velune/providers/adapters/lmstudio.py +175 -0
- velune/providers/adapters/ollama.py +233 -0
- velune/providers/adapters/openai.py +213 -0
- velune/providers/adapters/openrouter.py +81 -0
- velune/providers/adapters/together.py +134 -0
- velune/providers/adapters/xai.py +60 -0
- velune/providers/base.py +86 -0
- velune/providers/benchmarker.py +138 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +79 -0
- velune/providers/discovery/benchmarks.py +44 -0
- velune/providers/discovery/classifier.py +69 -0
- velune/providers/discovery/fireworks.py +95 -0
- velune/providers/discovery/gguf.py +88 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +117 -0
- velune/providers/discovery/groq.py +21 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +162 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +115 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/health.py +67 -0
- velune/providers/health_monitor.py +169 -0
- velune/providers/keystore.py +142 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +229 -0
- velune/providers/module.py +51 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +220 -0
- velune/providers/router.py +255 -0
- velune/providers/task_classifier.py +288 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +33 -0
- velune/repository/analyzer.py +127 -0
- velune/repository/ast_parser.py +822 -0
- velune/repository/blast_radius.py +298 -0
- velune/repository/boundary_classifier.py +295 -0
- velune/repository/cognition.py +316 -0
- velune/repository/grapher.py +179 -0
- velune/repository/import_graph.py +263 -0
- velune/repository/incremental_indexer.py +275 -0
- velune/repository/index_state.py +96 -0
- velune/repository/indexer.py +243 -0
- velune/repository/module.py +17 -0
- velune/repository/parser.py +474 -0
- velune/repository/project_type.py +300 -0
- velune/repository/rename_journal.py +287 -0
- velune/repository/scanner.py +193 -0
- velune/repository/schemas.py +102 -0
- velune/repository/symbol_registry.py +365 -0
- velune/repository/tracker.py +252 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/cache.py +110 -0
- velune/retrieval/fast_path.py +391 -0
- velune/retrieval/graph.py +124 -0
- velune/retrieval/hybrid.py +271 -0
- velune/retrieval/keyword.py +131 -0
- velune/retrieval/module.py +26 -0
- velune/retrieval/pipeline.py +303 -0
- velune/retrieval/reranker.py +102 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/slow_path.py +364 -0
- velune/retrieval/vector.py +203 -0
- velune/telemetry/__init__.py +59 -0
- velune/telemetry/cognition.py +267 -0
- velune/telemetry/cost_estimator.py +92 -0
- velune/telemetry/debug.py +304 -0
- velune/telemetry/doctor.py +244 -0
- velune/telemetry/logging.py +286 -0
- velune/telemetry/spans.py +277 -0
- velune/telemetry/token_tracker.py +140 -0
- velune/telemetry/usage_tracker.py +340 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +87 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +116 -0
- velune/tools/code/search.py +123 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +136 -0
- velune/tools/filesystem/write.py +163 -0
- velune/tools/git/history.py +177 -0
- velune/tools/git/operations.py +122 -0
- velune/tools/git/state.py +121 -0
- velune/tools/module.py +81 -0
- velune/tools/terminal/execute.py +72 -0
- velune/tools/terminal/history.py +47 -0
- velune/tools/web/fetch.py +55 -0
- velune/tools/web/validator.py +122 -0
- velune_cli-0.9.0.dist-info/METADATA +518 -0
- velune_cli-0.9.0.dist-info/RECORD +279 -0
- velune_cli-0.9.0.dist-info/WHEEL +4 -0
- velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
- 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]
|
velune/kernel/config.py
ADDED
|
@@ -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
|