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,25 @@
|
|
|
1
|
+
"""Memory-related errors."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class VeluneMemoryError(Exception):
|
|
5
|
+
"""Base exception for memory errors."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class VeluneMemoryStoreError(VeluneMemoryError):
|
|
11
|
+
"""Raised when memory store operation fails."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VeluneMemoryRetrievalError(VeluneMemoryError):
|
|
17
|
+
"""Raised when memory retrieval fails."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class VeluneMemoryConsolidationError(VeluneMemoryError):
|
|
23
|
+
"""Raised when memory consolidation fails."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Orchestration-related errors."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class OrchestrationError(Exception):
|
|
5
|
+
"""Base exception for orchestration errors."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentExecutionError(OrchestrationError):
|
|
11
|
+
"""Raised when agent execution fails."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PipelineExecutionError(OrchestrationError):
|
|
17
|
+
"""Raised when pipeline execution fails."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StateTransitionError(OrchestrationError):
|
|
23
|
+
"""Raised when state transition fails."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CheckpointError(OrchestrationError):
|
|
29
|
+
"""Raised when checkpoint operation fails."""
|
|
30
|
+
|
|
31
|
+
pass
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Provider-related errors."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ProviderError(Exception):
|
|
5
|
+
"""Base exception for provider errors."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProviderNotFoundError(ProviderError):
|
|
11
|
+
"""Raised when a provider is not found."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProviderConnectionError(ProviderError):
|
|
17
|
+
"""Raised when connection to provider fails."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProviderAuthenticationError(ProviderError):
|
|
23
|
+
"""Raised when provider authentication fails."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ModelNotFoundError(ProviderError):
|
|
29
|
+
"""Raised when a model is not found."""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class InferenceError(ProviderError):
|
|
35
|
+
"""Raised when inference fails."""
|
|
36
|
+
|
|
37
|
+
pass
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Coroutine
|
|
5
|
+
from typing import Any, TypeVar
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_loop() -> asyncio.AbstractEventLoop:
|
|
11
|
+
"""Return the running event loop, or a freshly created one."""
|
|
12
|
+
try:
|
|
13
|
+
return asyncio.get_running_loop()
|
|
14
|
+
except RuntimeError:
|
|
15
|
+
return asyncio.new_event_loop()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def submit(coro: Coroutine[Any, Any, T]) -> T:
|
|
19
|
+
"""Run *coro* to completion from a synchronous call site.
|
|
20
|
+
|
|
21
|
+
Raises RuntimeError if called from within a running event loop — callers
|
|
22
|
+
inside an async context should await the coroutine directly.
|
|
23
|
+
|
|
24
|
+
Delegates to ``velune.kernel.entrypoint.run_async`` so that
|
|
25
|
+
``asyncio.run`` is called from exactly one place in the codebase.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
asyncio.get_running_loop()
|
|
29
|
+
except RuntimeError:
|
|
30
|
+
from velune.kernel.entrypoint import run_async
|
|
31
|
+
|
|
32
|
+
return run_async(coro)
|
|
33
|
+
raise RuntimeError(
|
|
34
|
+
"submit() called from a running event loop — await the coroutine directly instead."
|
|
35
|
+
)
|
velune/core/logging.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Logging configuration for Velune."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import warnings
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
|
|
12
|
+
from rich.logging import RichHandler
|
|
13
|
+
|
|
14
|
+
from velune.core.trace import TracedLogger, _agent_id, _run_id, _step_id
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class JsonFormatter(logging.Formatter):
|
|
18
|
+
"""Production JSON log formatter that extracts context tracing attributes."""
|
|
19
|
+
|
|
20
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
21
|
+
exc_info = None
|
|
22
|
+
if record.exc_info:
|
|
23
|
+
exc_info = self.formatException(record.exc_info)
|
|
24
|
+
|
|
25
|
+
ts = datetime.now(UTC).isoformat().replace("+00:00", "")
|
|
26
|
+
|
|
27
|
+
log_data = {
|
|
28
|
+
"ts": ts,
|
|
29
|
+
"level": record.levelname,
|
|
30
|
+
"logger": record.name,
|
|
31
|
+
"msg": record.getMessage(),
|
|
32
|
+
"run_id": _run_id.get(),
|
|
33
|
+
"agent_id": _agent_id.get(),
|
|
34
|
+
"step_id": _step_id.get(),
|
|
35
|
+
}
|
|
36
|
+
if exc_info:
|
|
37
|
+
log_data["exception"] = exc_info
|
|
38
|
+
|
|
39
|
+
return json.dumps(log_data)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class LoggingConfig:
|
|
44
|
+
"""Logging configuration used during CLI bootstrap."""
|
|
45
|
+
|
|
46
|
+
level: str = "INFO"
|
|
47
|
+
show_path: bool = False
|
|
48
|
+
rich_tracebacks: bool = True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def configure_logging(config: LoggingConfig) -> None:
|
|
52
|
+
"""Configure process-wide logging once at startup."""
|
|
53
|
+
|
|
54
|
+
log_format = os.environ.get("VELUNE_LOG_FORMAT", "").lower()
|
|
55
|
+
|
|
56
|
+
if log_format == "json":
|
|
57
|
+
handler = logging.StreamHandler()
|
|
58
|
+
handler.setFormatter(JsonFormatter())
|
|
59
|
+
handlers: list[logging.Handler] = [handler]
|
|
60
|
+
else:
|
|
61
|
+
handlers = [
|
|
62
|
+
RichHandler(
|
|
63
|
+
rich_tracebacks=config.rich_tracebacks,
|
|
64
|
+
show_path=config.show_path,
|
|
65
|
+
omit_repeated_times=False,
|
|
66
|
+
)
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
logging.basicConfig(
|
|
70
|
+
level=getattr(logging, config.level.upper(), logging.INFO),
|
|
71
|
+
format="%(message)s" if log_format != "json" else None,
|
|
72
|
+
datefmt="[%X]" if log_format != "json" else None,
|
|
73
|
+
handlers=handlers,
|
|
74
|
+
force=True,
|
|
75
|
+
)
|
|
76
|
+
logging.captureWarnings(True)
|
|
77
|
+
warnings.simplefilter("default")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_logger(name: str) -> TracedLogger:
|
|
81
|
+
"""Get a namespaced Velune logger."""
|
|
82
|
+
|
|
83
|
+
return TracedLogger(name)
|
velune/core/paths.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Platform-compliant storage paths for Velune.
|
|
2
|
+
|
|
3
|
+
Heavy, frequently-written state (the SQLite cognitive core and the Qdrant
|
|
4
|
+
vector store) must NOT live inside the workspace tree. On Windows the
|
|
5
|
+
workspace is often under a cloud-synced folder (OneDrive, Dropbox, Google
|
|
6
|
+
Drive). Every file touch then triggers a sync-engine reparse, multiplying
|
|
7
|
+
I/O latency by 3-10x and serializing startup behind metadata syncs. This was
|
|
8
|
+
the dominant contributor to Velune's ~78s cold start.
|
|
9
|
+
|
|
10
|
+
This module centralizes storage resolution so all subsystems agree on one
|
|
11
|
+
location, and relocates heavy state to the platform-native, *non-synced*
|
|
12
|
+
application-data directory:
|
|
13
|
+
|
|
14
|
+
Windows -> %LOCALAPPDATA%\\Velune
|
|
15
|
+
macOS -> ~/Library/Application Support/Velune
|
|
16
|
+
Linux -> $XDG_DATA_HOME/velune (default ~/.local/share/velune)
|
|
17
|
+
|
|
18
|
+
State is still isolated *per workspace* (so two projects never share a
|
|
19
|
+
cognitive core) by hashing the workspace's resolved absolute path into a
|
|
20
|
+
stable, human-readable slug under ``<data_root>/workspaces/``.
|
|
21
|
+
|
|
22
|
+
Lightweight, human-facing files (``velune.toml``, ``.veluneignore``,
|
|
23
|
+
snapshots, sessions) intentionally stay in the workspace ``.velune/`` dir —
|
|
24
|
+
they're tiny, rarely written, and users expect them project-local.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import hashlib
|
|
30
|
+
import logging
|
|
31
|
+
import os
|
|
32
|
+
import platform
|
|
33
|
+
import re
|
|
34
|
+
import shutil
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger("velune.core.paths")
|
|
38
|
+
|
|
39
|
+
_APP_NAME = "Velune"
|
|
40
|
+
|
|
41
|
+
# Heavy state filenames relocated off the workspace tree.
|
|
42
|
+
COGNITIVE_DB_NAME = "velune_cognitive_core.db"
|
|
43
|
+
QDRANT_STORE_NAME = "qdrant_local_store"
|
|
44
|
+
|
|
45
|
+
# Marker written into a relocated workspace dir once legacy data has been
|
|
46
|
+
# migrated, so migration runs at most once per workspace.
|
|
47
|
+
_MIGRATION_MARKER = ".migrated_from_workspace"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def app_data_root() -> Path:
|
|
51
|
+
"""Return the platform-native, non-synced application data root.
|
|
52
|
+
|
|
53
|
+
Honors ``VELUNE_DATA_HOME`` as an explicit override (useful for tests,
|
|
54
|
+
CI, and power users who want all state in one place).
|
|
55
|
+
"""
|
|
56
|
+
override = os.environ.get("VELUNE_DATA_HOME")
|
|
57
|
+
if override:
|
|
58
|
+
return Path(override).expanduser()
|
|
59
|
+
|
|
60
|
+
system = platform.system()
|
|
61
|
+
if system == "Windows":
|
|
62
|
+
base = os.environ.get("LOCALAPPDATA") or (Path.home() / "AppData" / "Local")
|
|
63
|
+
return Path(base) / _APP_NAME
|
|
64
|
+
if system == "Darwin":
|
|
65
|
+
return Path.home() / "Library" / "Application Support" / _APP_NAME
|
|
66
|
+
# Linux / other POSIX
|
|
67
|
+
xdg = os.environ.get("XDG_DATA_HOME")
|
|
68
|
+
base = Path(xdg) if xdg else (Path.home() / ".local" / "share")
|
|
69
|
+
return base / "velune"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _workspace_slug(workspace: Path) -> str:
|
|
73
|
+
"""Build a stable, collision-resistant, readable slug for *workspace*.
|
|
74
|
+
|
|
75
|
+
Combines a sanitized directory name with a short hash of the resolved
|
|
76
|
+
absolute path so distinct projects with the same folder name never
|
|
77
|
+
collide.
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
resolved = workspace.resolve()
|
|
81
|
+
except Exception:
|
|
82
|
+
resolved = workspace.absolute()
|
|
83
|
+
digest = hashlib.sha1(str(resolved).encode("utf-8")).hexdigest()[:10]
|
|
84
|
+
name = re.sub(r"[^A-Za-z0-9._-]", "_", resolved.name) or "workspace"
|
|
85
|
+
return f"{name}-{digest}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def workspace_storage_dir(workspace: Path) -> Path:
|
|
89
|
+
"""Return (creating) the non-synced storage dir for *workspace*."""
|
|
90
|
+
target = app_data_root() / "workspaces" / _workspace_slug(workspace)
|
|
91
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
return target
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def cognitive_db_path(workspace: Path) -> Path:
|
|
96
|
+
"""Absolute path to the workspace's SQLite cognitive core (non-synced)."""
|
|
97
|
+
return workspace_storage_dir(workspace) / COGNITIVE_DB_NAME
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def qdrant_store_path(workspace: Path) -> Path:
|
|
101
|
+
"""Absolute path to the workspace's Qdrant local store (non-synced)."""
|
|
102
|
+
return workspace_storage_dir(workspace) / QDRANT_STORE_NAME
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
LANCEDB_STORE_NAME = "lancedb_semantic_store"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def lancedb_store_path(workspace: Path) -> Path:
|
|
109
|
+
"""Absolute path to the workspace's LanceDB semantic store (non-synced)."""
|
|
110
|
+
return workspace_storage_dir(workspace) / LANCEDB_STORE_NAME
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def legacy_workspace_dir(workspace: Path) -> Path:
|
|
114
|
+
"""The old in-workspace ``.velune`` directory (may be cloud-synced)."""
|
|
115
|
+
return workspace / ".velune"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def migrate_legacy_storage(workspace: Path) -> bool:
|
|
119
|
+
"""One-time, best-effort relocation of heavy state out of the workspace.
|
|
120
|
+
|
|
121
|
+
Copies (does not move) any pre-existing ``.velune/velune_cognitive_core.db``
|
|
122
|
+
and ``.velune/qdrant_local_store`` into the non-synced storage dir, leaving
|
|
123
|
+
the originals untouched as a safety backup. Writes a marker so this runs at
|
|
124
|
+
most once per workspace. Returns True if anything was migrated.
|
|
125
|
+
|
|
126
|
+
Copy-then-mark (rather than move) is deliberate: an interrupted move could
|
|
127
|
+
corrupt a live cognitive core, whereas a partial copy is simply retried.
|
|
128
|
+
"""
|
|
129
|
+
target = workspace_storage_dir(workspace)
|
|
130
|
+
marker = target / _MIGRATION_MARKER
|
|
131
|
+
if marker.exists():
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
legacy = legacy_workspace_dir(workspace)
|
|
135
|
+
migrated = False
|
|
136
|
+
|
|
137
|
+
legacy_db = legacy / COGNITIVE_DB_NAME
|
|
138
|
+
new_db = target / COGNITIVE_DB_NAME
|
|
139
|
+
if legacy_db.exists() and not new_db.exists():
|
|
140
|
+
try:
|
|
141
|
+
# Bring along WAL/SHM sidecars if present for a consistent copy.
|
|
142
|
+
for suffix in ("", "-wal", "-shm"):
|
|
143
|
+
src = legacy / f"{COGNITIVE_DB_NAME}{suffix}"
|
|
144
|
+
if src.exists():
|
|
145
|
+
shutil.copy2(src, target / src.name)
|
|
146
|
+
migrated = True
|
|
147
|
+
logger.info("Migrated cognitive core from %s -> %s", legacy_db, new_db)
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
logger.warning("Cognitive core migration skipped: %s", exc)
|
|
150
|
+
|
|
151
|
+
legacy_qdrant = legacy / QDRANT_STORE_NAME
|
|
152
|
+
new_qdrant = target / QDRANT_STORE_NAME
|
|
153
|
+
if legacy_qdrant.is_dir() and not new_qdrant.exists():
|
|
154
|
+
try:
|
|
155
|
+
shutil.copytree(legacy_qdrant, new_qdrant)
|
|
156
|
+
migrated = True
|
|
157
|
+
logger.info("Migrated vector store from %s -> %s", legacy_qdrant, new_qdrant)
|
|
158
|
+
except Exception as exc:
|
|
159
|
+
logger.warning("Vector store migration skipped: %s", exc)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
marker.write_text("ok\n", encoding="utf-8")
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
return migrated
|
velune/core/runtime.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Process runtime bootstrap for Velune Cognitive OS CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from velune.kernel.config import ConfigService, VeluneConfig, get_default_config
|
|
12
|
+
from velune.kernel.lifecycle import LifecycleCoordinator
|
|
13
|
+
from velune.kernel.registry import ServiceContainer
|
|
14
|
+
|
|
15
|
+
# Stateful Orchestration & Memory Tiers
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(slots=True)
|
|
19
|
+
class RuntimeContext:
|
|
20
|
+
"""Runtime resources shared across CLI commands."""
|
|
21
|
+
|
|
22
|
+
workspace: Path
|
|
23
|
+
config_path: Path | None
|
|
24
|
+
config: VeluneConfig
|
|
25
|
+
console: Console
|
|
26
|
+
logger_name: str
|
|
27
|
+
container: ServiceContainer
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_runtime(
|
|
31
|
+
workspace: Path,
|
|
32
|
+
config_path: Path | None = None,
|
|
33
|
+
verbose: bool = False,
|
|
34
|
+
) -> RuntimeContext:
|
|
35
|
+
"""Create and register the shared runtime services using the declarative bootstrapper."""
|
|
36
|
+
from velune.core.startup_profiler import mark
|
|
37
|
+
|
|
38
|
+
mark("build_runtime: enter")
|
|
39
|
+
# 1. Setup Logging and Console
|
|
40
|
+
console = Console(highlight=False)
|
|
41
|
+
logger_name = "velune"
|
|
42
|
+
logger = logging.getLogger(logger_name)
|
|
43
|
+
|
|
44
|
+
# Configure root logger levels. Non-verbose hides internal INFO/DEBUG
|
|
45
|
+
# logs so users only ever see Rich-formatted output, not raw Python logs.
|
|
46
|
+
logging.basicConfig(level=logging.DEBUG if verbose else logging.WARNING)
|
|
47
|
+
logger.setLevel(logging.DEBUG if verbose else logging.WARNING)
|
|
48
|
+
|
|
49
|
+
# 2. Configuration Service
|
|
50
|
+
config_service = ConfigService(workspace=workspace, config_path=config_path)
|
|
51
|
+
try:
|
|
52
|
+
config = config_service.load()
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.warning("Failed to load configuration. Falling back to system defaults: %s", e)
|
|
55
|
+
config = get_default_config()
|
|
56
|
+
mark("config loaded")
|
|
57
|
+
|
|
58
|
+
container = ServiceContainer()
|
|
59
|
+
lifecycle = LifecycleCoordinator()
|
|
60
|
+
lifecycle.container = container
|
|
61
|
+
|
|
62
|
+
# Pre-register basic primitive context dependencies
|
|
63
|
+
container.register_instance("runtime.config", config)
|
|
64
|
+
container.register_instance("runtime.config_service", config_service)
|
|
65
|
+
container.register_instance("runtime.console", console)
|
|
66
|
+
container.register_instance("runtime.logger", logger)
|
|
67
|
+
container.register_instance("runtime.workspace", workspace)
|
|
68
|
+
container.register_instance("runtime.config_path", config_path)
|
|
69
|
+
container.register_instance("runtime.lifecycle", lifecycle)
|
|
70
|
+
|
|
71
|
+
# Detect and persist GPU info
|
|
72
|
+
try:
|
|
73
|
+
from velune.providers.discovery.gpu import GPUDetector
|
|
74
|
+
|
|
75
|
+
gpu_info = GPUDetector().detect()
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.warning("GPU detection failed, using safe defaults: %s", e)
|
|
78
|
+
gpu_info = {
|
|
79
|
+
"has_gpu": False,
|
|
80
|
+
"gpu_type": None,
|
|
81
|
+
"vram_total_gb": None,
|
|
82
|
+
"vram_free_gb": None,
|
|
83
|
+
"cuda_available": False,
|
|
84
|
+
}
|
|
85
|
+
container.register_instance("runtime.gpu_info", gpu_info)
|
|
86
|
+
mark("gpu detected")
|
|
87
|
+
|
|
88
|
+
# 3. Initialize and bootstrap declarative subsystems in dependency order
|
|
89
|
+
from velune.kernel.bootstrap import RuntimeBootstrapper, RuntimeEnvironment
|
|
90
|
+
from velune.kernel.modules import ALL_MODULES
|
|
91
|
+
|
|
92
|
+
env = RuntimeEnvironment(
|
|
93
|
+
workspace=workspace,
|
|
94
|
+
config=config,
|
|
95
|
+
container=container,
|
|
96
|
+
lifecycle=lifecycle,
|
|
97
|
+
verbose=verbose,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
bootstrapper = RuntimeBootstrapper()
|
|
101
|
+
for module in ALL_MODULES:
|
|
102
|
+
bootstrapper.register_module(module)
|
|
103
|
+
bootstrapper.bootstrap(env)
|
|
104
|
+
mark("subsystems bootstrapped")
|
|
105
|
+
|
|
106
|
+
return RuntimeContext(
|
|
107
|
+
workspace=workspace,
|
|
108
|
+
config_path=config_path,
|
|
109
|
+
config=config,
|
|
110
|
+
console=console,
|
|
111
|
+
logger_name=logger_name,
|
|
112
|
+
container=container,
|
|
113
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Lightweight startup phase profiler.
|
|
2
|
+
|
|
3
|
+
Enable with ``VELUNE_PROFILE_STARTUP=1`` to print a per-phase timing breakdown
|
|
4
|
+
of the boot path to stderr. Each ``mark()`` prints immediately (with the delta
|
|
5
|
+
since the previous mark), so the breakdown survives even if startup crashes
|
|
6
|
+
mid-phase — invaluable for diagnosing where the time goes on a given machine.
|
|
7
|
+
|
|
8
|
+
Zero overhead when disabled: ``mark()`` short-circuits on the first line.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import atexit
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
_ENABLED = os.environ.get("VELUNE_PROFILE_STARTUP", "").lower() in ("1", "true", "yes")
|
|
19
|
+
_PROCESS_START = time.perf_counter()
|
|
20
|
+
_marks: list[tuple[str, float]] = []
|
|
21
|
+
_reported = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def enabled() -> bool:
|
|
25
|
+
return _ENABLED
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def mark(label: str) -> None:
|
|
29
|
+
"""Record and print a startup phase boundary (no-op unless enabled)."""
|
|
30
|
+
if not _ENABLED:
|
|
31
|
+
return
|
|
32
|
+
now = time.perf_counter()
|
|
33
|
+
prev = _marks[-1][1] if _marks else _PROCESS_START
|
|
34
|
+
_marks.append((label, now))
|
|
35
|
+
delta = now - prev
|
|
36
|
+
elapsed = now - _PROCESS_START
|
|
37
|
+
print(
|
|
38
|
+
f"[velune startup] +{elapsed:6.3f}s Δ{delta:6.3f}s {label}",
|
|
39
|
+
file=sys.stderr,
|
|
40
|
+
flush=True,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def report() -> None:
|
|
45
|
+
"""Print a final total. Registered via atexit; safe to call directly."""
|
|
46
|
+
global _reported
|
|
47
|
+
if not _ENABLED or _reported:
|
|
48
|
+
return
|
|
49
|
+
_reported = True
|
|
50
|
+
total = time.perf_counter() - _PROCESS_START
|
|
51
|
+
print(f"[velune startup] ── total {total:.3f}s ──", file=sys.stderr, flush=True)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if _ENABLED:
|
|
55
|
+
atexit.register(report)
|
|
56
|
+
mark("interpreter -> profiler import")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Structured concurrency manager for tracking, timing out, and cancelling background tasks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import threading
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
9
|
+
from typing import Any, TypeVar
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
logger = logging.getLogger("velune.core.task_registry")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BackgroundTaskRegistry:
|
|
16
|
+
"""Tracks in-flight asyncio tasks with timeout and cancellation support."""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self._tasks: dict[str, asyncio.Task] = {}
|
|
20
|
+
self._lock = threading.Lock()
|
|
21
|
+
self._submitted_count = 0
|
|
22
|
+
self._dropped_count = 0
|
|
23
|
+
|
|
24
|
+
def submit(
|
|
25
|
+
self,
|
|
26
|
+
name: str,
|
|
27
|
+
coro: Awaitable[T],
|
|
28
|
+
timeout_seconds: float = 60.0,
|
|
29
|
+
on_error: Callable[[Exception], None] | None = None,
|
|
30
|
+
) -> asyncio.Task | None:
|
|
31
|
+
"""Submit a named background task from an async context only."""
|
|
32
|
+
try:
|
|
33
|
+
running_loop = asyncio.get_running_loop()
|
|
34
|
+
except RuntimeError:
|
|
35
|
+
logger.error(
|
|
36
|
+
"BackgroundTaskRegistry.submit('%s') called outside an async context. "
|
|
37
|
+
"Background tasks must be submitted from within a running event loop.",
|
|
38
|
+
name,
|
|
39
|
+
)
|
|
40
|
+
with self._lock:
|
|
41
|
+
self._dropped_count += 1
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
with self._lock:
|
|
45
|
+
self._submitted_count += 1
|
|
46
|
+
|
|
47
|
+
async def _wrapped() -> Any:
|
|
48
|
+
try:
|
|
49
|
+
result = await asyncio.wait_for(coro, timeout=timeout_seconds)
|
|
50
|
+
logger.debug("Background task '%s' completed.", name)
|
|
51
|
+
return result
|
|
52
|
+
except TimeoutError:
|
|
53
|
+
logger.warning("Background task '%s' timed out after %.1fs", name, timeout_seconds)
|
|
54
|
+
except asyncio.CancelledError:
|
|
55
|
+
raise
|
|
56
|
+
except Exception as exc:
|
|
57
|
+
logger.error("Background task '%s' failed: %s", name, exc)
|
|
58
|
+
if on_error:
|
|
59
|
+
try:
|
|
60
|
+
on_error(exc)
|
|
61
|
+
except Exception as inner:
|
|
62
|
+
logger.error("Error handler for task '%s' failed: %s", name, inner)
|
|
63
|
+
finally:
|
|
64
|
+
with self._lock:
|
|
65
|
+
self._tasks.pop(name, None)
|
|
66
|
+
|
|
67
|
+
task = running_loop.create_task(_wrapped(), name=name)
|
|
68
|
+
with self._lock:
|
|
69
|
+
self._tasks[name] = task
|
|
70
|
+
return task
|
|
71
|
+
|
|
72
|
+
async def cancel_all(self, timeout: float = 5.0) -> None:
|
|
73
|
+
"""Cancel all pending tasks and wait for completion."""
|
|
74
|
+
with self._lock:
|
|
75
|
+
tasks = [t for t in self._tasks.values() if not t.done()]
|
|
76
|
+
|
|
77
|
+
if not tasks:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
for task in tasks:
|
|
81
|
+
task.cancel()
|
|
82
|
+
|
|
83
|
+
# Give tasks a chance to handle CancelledError
|
|
84
|
+
done, pending = await asyncio.wait(tasks, timeout=timeout)
|
|
85
|
+
|
|
86
|
+
if pending:
|
|
87
|
+
logger.warning("%d background tasks did not cancel within %.1fs", len(pending), timeout)
|
|
88
|
+
|
|
89
|
+
# Shutdown stats reporting of dropped tasks
|
|
90
|
+
stats = self.stats()
|
|
91
|
+
if stats["dropped"] > 0:
|
|
92
|
+
logger.info("Task registry shutdown: %d dropped tasks reported.", stats["dropped"])
|
|
93
|
+
|
|
94
|
+
with self._lock:
|
|
95
|
+
self._tasks.clear()
|
|
96
|
+
|
|
97
|
+
def pending_count(self) -> int:
|
|
98
|
+
"""Expose pending tasks count for health checks."""
|
|
99
|
+
with self._lock:
|
|
100
|
+
return len([t for t in self._tasks.values() if not t.done()])
|
|
101
|
+
|
|
102
|
+
def is_healthy(self) -> bool:
|
|
103
|
+
"""Check if task registry is healthy."""
|
|
104
|
+
try:
|
|
105
|
+
loop = asyncio.get_running_loop()
|
|
106
|
+
loop_alive = loop.is_running()
|
|
107
|
+
except Exception:
|
|
108
|
+
loop_alive = False
|
|
109
|
+
return loop_alive and self.pending_count() < 50
|
|
110
|
+
|
|
111
|
+
def stats(self) -> dict[str, int]:
|
|
112
|
+
"""Return task submission telemetry stats."""
|
|
113
|
+
with self._lock:
|
|
114
|
+
return {
|
|
115
|
+
"submitted": self._submitted_count,
|
|
116
|
+
"dropped": self._dropped_count,
|
|
117
|
+
}
|