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
velune/daemon/server.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from velune.daemon.transport import DAEMON_PID_FILE, IpcServer, get_ipc_address
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VeluneDaemon:
|
|
9
|
+
"""Background daemon holding initialized subsystems."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, workspace: Path):
|
|
12
|
+
self.workspace = workspace
|
|
13
|
+
self.runtime = None # RuntimeContext
|
|
14
|
+
self._server = None
|
|
15
|
+
|
|
16
|
+
async def start(self):
|
|
17
|
+
"""Initialize runtime and start IPC server."""
|
|
18
|
+
from velune.core.runtime import build_runtime
|
|
19
|
+
|
|
20
|
+
self.runtime = build_runtime(self.workspace)
|
|
21
|
+
await self.runtime.container.get("runtime.lifecycle").startup()
|
|
22
|
+
|
|
23
|
+
self._server = IpcServer(get_ipc_address(), self._dispatch)
|
|
24
|
+
await self._server.start()
|
|
25
|
+
|
|
26
|
+
# Write PID file
|
|
27
|
+
DAEMON_PID_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
DAEMON_PID_FILE.write_text(str(os.getpid()))
|
|
29
|
+
|
|
30
|
+
await self._server.serve_forever()
|
|
31
|
+
|
|
32
|
+
def shutdown(self):
|
|
33
|
+
if self._server:
|
|
34
|
+
self._server.close()
|
|
35
|
+
if DAEMON_PID_FILE.exists():
|
|
36
|
+
try:
|
|
37
|
+
DAEMON_PID_FILE.unlink()
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
async def _dispatch(self, request: dict) -> dict:
|
|
42
|
+
"""Route IPC request to appropriate handler."""
|
|
43
|
+
command = request.get("command")
|
|
44
|
+
if command == "ask":
|
|
45
|
+
return await self._handle_ask(request)
|
|
46
|
+
elif command == "models_list":
|
|
47
|
+
return await self._handle_models_list(request)
|
|
48
|
+
elif command == "probe_model":
|
|
49
|
+
return await self._handle_probe_model(request)
|
|
50
|
+
elif command == "ping":
|
|
51
|
+
return {"status": "ok", "pid": os.getpid()}
|
|
52
|
+
return {"error": f"Unknown command: {command}"}
|
|
53
|
+
|
|
54
|
+
async def _handle_ask(self, request: dict) -> dict:
|
|
55
|
+
# Phase 2 implementation placeholder
|
|
56
|
+
return {"status": "success", "response": "Phase 2 daemon execution."}
|
|
57
|
+
|
|
58
|
+
async def _handle_models_list(self, request: dict) -> dict:
|
|
59
|
+
try:
|
|
60
|
+
models = self.runtime.container.get("runtime.model_registry").list_all()
|
|
61
|
+
return {
|
|
62
|
+
"status": "success",
|
|
63
|
+
"models": [m.to_dict() if hasattr(m, "to_dict") else str(m) for m in models],
|
|
64
|
+
}
|
|
65
|
+
except Exception as e:
|
|
66
|
+
return {"status": "error", "message": str(e)}
|
|
67
|
+
|
|
68
|
+
async def _handle_probe_model(self, request: dict) -> dict:
|
|
69
|
+
model_id = request.get("model_id")
|
|
70
|
+
provider_id = request.get("provider_id")
|
|
71
|
+
if not model_id or not provider_id:
|
|
72
|
+
return {"status": "error", "message": "Missing model_id or provider_id"}
|
|
73
|
+
|
|
74
|
+
async def run_probe():
|
|
75
|
+
try:
|
|
76
|
+
provider_reg = self.runtime.container.get("runtime.provider_registry")
|
|
77
|
+
provider = provider_reg.get(provider_id)
|
|
78
|
+
if not provider:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
from velune.models.probes import ModelProber
|
|
82
|
+
from velune.models.profile_cache import ModelProfileCache
|
|
83
|
+
|
|
84
|
+
profile_cache = ModelProfileCache(
|
|
85
|
+
self.workspace / ".velune" / "model_profiles.json"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
prober = ModelProber(provider, model_id)
|
|
89
|
+
results = await prober.run_all_probes()
|
|
90
|
+
profile_cache.set(model_id, provider_id, results)
|
|
91
|
+
|
|
92
|
+
# Also apply in-memory to daemon's registry
|
|
93
|
+
registry = self.runtime.container.get("runtime.model_registry")
|
|
94
|
+
if registry:
|
|
95
|
+
model = registry.get(model_id, provider_id)
|
|
96
|
+
if model:
|
|
97
|
+
registry._apply_probe_results(model, results)
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
asyncio.create_task(run_probe())
|
|
102
|
+
return {
|
|
103
|
+
"status": "success",
|
|
104
|
+
"message": f"Started probing for model {model_id} in daemon background.",
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
import sys
|
|
110
|
+
|
|
111
|
+
from velune.daemon.client import DaemonClient
|
|
112
|
+
from velune.kernel.entrypoint import run_async
|
|
113
|
+
|
|
114
|
+
if len(sys.argv) < 2:
|
|
115
|
+
print("Usage: python -m velune.daemon.server <workspace_path>")
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
|
|
118
|
+
if DaemonClient.is_running():
|
|
119
|
+
print("Daemon is already running.")
|
|
120
|
+
sys.exit(0)
|
|
121
|
+
|
|
122
|
+
workspace_path = Path(sys.argv[1]).resolve()
|
|
123
|
+
daemon = VeluneDaemon(workspace_path)
|
|
124
|
+
try:
|
|
125
|
+
run_async(daemon.start())
|
|
126
|
+
except KeyboardInterrupt:
|
|
127
|
+
daemon.shutdown()
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import socket
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
DAEMON_SOCKET_PATH = Path.home() / ".velune" / "daemon.sock"
|
|
10
|
+
DAEMON_PID_FILE = Path.home() / ".velune" / "daemon.pid"
|
|
11
|
+
DAEMON_PIPE_ADDRESS = r"\\.\pipe\velune_daemon"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_ipc_address() -> str:
|
|
15
|
+
if sys.platform == "win32":
|
|
16
|
+
return DAEMON_PIPE_ADDRESS
|
|
17
|
+
return str(DAEMON_SOCKET_PATH)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class IpcServer:
|
|
21
|
+
def __init__(self, address: str, handle_callback: Callable[[dict[str, Any]], Any]):
|
|
22
|
+
self.address = address
|
|
23
|
+
self.handle_callback = handle_callback
|
|
24
|
+
self._server = None
|
|
25
|
+
self._win_server = None
|
|
26
|
+
|
|
27
|
+
async def start(self):
|
|
28
|
+
# Ensure directories exist
|
|
29
|
+
if sys.platform != "win32":
|
|
30
|
+
socket_path = Path(self.address)
|
|
31
|
+
socket_path.parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
if socket_path.exists():
|
|
33
|
+
try:
|
|
34
|
+
socket_path.unlink()
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
self._server = await asyncio.start_unix_server(
|
|
39
|
+
self._handle_unix_client, path=self.address
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
self._win_server = _WindowsNamedPipeServer(self.address, self.handle_callback)
|
|
43
|
+
self._win_server.start()
|
|
44
|
+
|
|
45
|
+
async def serve_forever(self):
|
|
46
|
+
if sys.platform != "win32":
|
|
47
|
+
await self._server.serve_forever()
|
|
48
|
+
else:
|
|
49
|
+
await self._win_server.serve_forever()
|
|
50
|
+
|
|
51
|
+
def close(self):
|
|
52
|
+
if self._server:
|
|
53
|
+
self._server.close()
|
|
54
|
+
if self._win_server:
|
|
55
|
+
self._win_server.close()
|
|
56
|
+
|
|
57
|
+
async def _handle_unix_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
|
58
|
+
try:
|
|
59
|
+
data = await reader.read(65536)
|
|
60
|
+
if not data:
|
|
61
|
+
return
|
|
62
|
+
request = json.loads(data.decode("utf-8"))
|
|
63
|
+
response = await self.handle_callback(request)
|
|
64
|
+
writer.write(json.dumps(response).encode("utf-8"))
|
|
65
|
+
await writer.drain()
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
finally:
|
|
69
|
+
writer.close()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class _WindowsNamedPipeServer:
|
|
73
|
+
def __init__(self, address: str, handle_callback: Callable[[dict[str, Any]], Any]):
|
|
74
|
+
self.address = address
|
|
75
|
+
self.handle_callback = handle_callback
|
|
76
|
+
self.listener = None
|
|
77
|
+
self.is_running = False
|
|
78
|
+
|
|
79
|
+
def start(self):
|
|
80
|
+
from multiprocessing.connection import Listener
|
|
81
|
+
|
|
82
|
+
self.listener = Listener(self.address, "AF_PIPE")
|
|
83
|
+
self.is_running = True
|
|
84
|
+
|
|
85
|
+
def close(self):
|
|
86
|
+
self.is_running = False
|
|
87
|
+
# Wake up the blocked accept() thread with a dummy connection on Windows
|
|
88
|
+
try:
|
|
89
|
+
from multiprocessing.connection import Client
|
|
90
|
+
|
|
91
|
+
conn = Client(self.address, "AF_PIPE")
|
|
92
|
+
conn.close()
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
if self.listener:
|
|
96
|
+
try:
|
|
97
|
+
self.listener.close()
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
async def serve_forever(self):
|
|
102
|
+
loop = asyncio.get_running_loop()
|
|
103
|
+
while self.is_running:
|
|
104
|
+
try:
|
|
105
|
+
conn = await loop.run_in_executor(None, self.listener.accept)
|
|
106
|
+
asyncio.create_task(self._handle_conn(conn))
|
|
107
|
+
except Exception:
|
|
108
|
+
if not self.is_running:
|
|
109
|
+
break
|
|
110
|
+
await asyncio.sleep(0.1)
|
|
111
|
+
|
|
112
|
+
async def _handle_conn(self, conn):
|
|
113
|
+
try:
|
|
114
|
+
loop = asyncio.get_running_loop()
|
|
115
|
+
data = await loop.run_in_executor(None, conn.recv)
|
|
116
|
+
request = json.loads(data)
|
|
117
|
+
response = await self.handle_callback(request)
|
|
118
|
+
await loop.run_in_executor(None, conn.send, json.dumps(response))
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
finally:
|
|
122
|
+
try:
|
|
123
|
+
conn.close()
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class IpcClient:
|
|
129
|
+
@staticmethod
|
|
130
|
+
def is_running(address: str) -> bool:
|
|
131
|
+
if sys.platform != "win32":
|
|
132
|
+
socket_path = Path(address)
|
|
133
|
+
if not socket_path.exists():
|
|
134
|
+
return False
|
|
135
|
+
try:
|
|
136
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
137
|
+
sock.settimeout(1.0)
|
|
138
|
+
sock.connect(address)
|
|
139
|
+
sock.close()
|
|
140
|
+
return True
|
|
141
|
+
except Exception:
|
|
142
|
+
return False
|
|
143
|
+
else:
|
|
144
|
+
from multiprocessing.connection import Client
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
conn = Client(address, "AF_PIPE")
|
|
148
|
+
conn.close()
|
|
149
|
+
return True
|
|
150
|
+
except Exception:
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
async def send_command(address: str, command: str, **kwargs) -> dict:
|
|
155
|
+
request = {"command": command, **kwargs}
|
|
156
|
+
if sys.platform != "win32":
|
|
157
|
+
reader, writer = await asyncio.open_unix_connection(address)
|
|
158
|
+
try:
|
|
159
|
+
writer.write(json.dumps(request).encode("utf-8"))
|
|
160
|
+
await writer.drain()
|
|
161
|
+
data = await reader.read(65536)
|
|
162
|
+
return json.loads(data.decode("utf-8"))
|
|
163
|
+
finally:
|
|
164
|
+
writer.close()
|
|
165
|
+
else:
|
|
166
|
+
from multiprocessing.connection import Client
|
|
167
|
+
|
|
168
|
+
loop = asyncio.get_running_loop()
|
|
169
|
+
|
|
170
|
+
def _send():
|
|
171
|
+
conn = Client(address, "AF_PIPE")
|
|
172
|
+
try:
|
|
173
|
+
conn.send(json.dumps(request))
|
|
174
|
+
res = conn.recv()
|
|
175
|
+
return json.loads(res)
|
|
176
|
+
finally:
|
|
177
|
+
conn.close()
|
|
178
|
+
|
|
179
|
+
return await loop.run_in_executor(None, _send)
|
velune/events.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Unified event types and CognitiveBus for Velune system."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import fnmatch
|
|
7
|
+
import logging
|
|
8
|
+
import uuid
|
|
9
|
+
from collections import deque
|
|
10
|
+
from collections.abc import AsyncIterator, Callable
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("velune.events")
|
|
17
|
+
|
|
18
|
+
_HISTORY_MAXLEN = 1000
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Event(BaseModel):
|
|
22
|
+
"""The central message token in the event bus."""
|
|
23
|
+
|
|
24
|
+
event_id: str = Field(default_factory=lambda: f"evt-{uuid.uuid4().hex[:12]}")
|
|
25
|
+
event_type: str
|
|
26
|
+
timestamp: float = Field(default_factory=lambda: datetime.now(tz=UTC).timestamp())
|
|
27
|
+
source: str
|
|
28
|
+
data: dict[str, Any] = Field(default_factory=dict)
|
|
29
|
+
correlation_id: str | None = None
|
|
30
|
+
|
|
31
|
+
class Config:
|
|
32
|
+
frozen = True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
EventHandler = Callable[[Event], None | Any]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Subscription:
|
|
39
|
+
"""Subscription token allowing easy unsubscribe mechanics."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, bus: CognitiveBus, event_type: str, handler: EventHandler) -> None:
|
|
42
|
+
self.bus = bus
|
|
43
|
+
self.event_type = event_type
|
|
44
|
+
self.handler = handler
|
|
45
|
+
|
|
46
|
+
def unsubscribe(self) -> None:
|
|
47
|
+
"""Cancel this subscription."""
|
|
48
|
+
self.bus.unsubscribe(self.event_type, self.handler)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CognitiveBus:
|
|
52
|
+
"""Async event bus supporting wildcard routing, replay, and correlation waiting."""
|
|
53
|
+
|
|
54
|
+
def __init__(self) -> None:
|
|
55
|
+
self._subscribers: dict[str, set[EventHandler]] = {}
|
|
56
|
+
self._queue: asyncio.Queue[Event] = asyncio.Queue()
|
|
57
|
+
self._running: bool = False
|
|
58
|
+
self._dispatch_task: asyncio.Task | None = None
|
|
59
|
+
self._history: deque[Event] = deque(maxlen=_HISTORY_MAXLEN)
|
|
60
|
+
self._pending_responses: dict[str, asyncio.Future[Event]] = {}
|
|
61
|
+
|
|
62
|
+
async def emit(self, event: Event) -> None:
|
|
63
|
+
"""Enqueue/Publish a kernel event to the bus."""
|
|
64
|
+
if len(self._history) >= _HISTORY_MAXLEN:
|
|
65
|
+
oldest = self._history[0]
|
|
66
|
+
logger.debug(
|
|
67
|
+
"Event history at capacity (%d); dropping oldest: %s [%s]",
|
|
68
|
+
_HISTORY_MAXLEN,
|
|
69
|
+
oldest.event_type,
|
|
70
|
+
oldest.event_id,
|
|
71
|
+
)
|
|
72
|
+
self._history.append(event)
|
|
73
|
+
|
|
74
|
+
if event.correlation_id and event.correlation_id in self._pending_responses:
|
|
75
|
+
future = self._pending_responses[event.correlation_id]
|
|
76
|
+
if not future.done():
|
|
77
|
+
future.set_result(event)
|
|
78
|
+
|
|
79
|
+
if not self._running:
|
|
80
|
+
await self._dispatch_immediate(event)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
await self._queue.put(event)
|
|
84
|
+
|
|
85
|
+
async def subscribe(self, event_type: str, handler: EventHandler) -> Subscription:
|
|
86
|
+
"""Subscribe a handler to event types (supports wildcards like 'Memory*')."""
|
|
87
|
+
if event_type not in self._subscribers:
|
|
88
|
+
self._subscribers[event_type] = set()
|
|
89
|
+
self._subscribers[event_type].add(handler)
|
|
90
|
+
logger.debug("Subscribed to pattern '%s'", event_type)
|
|
91
|
+
return Subscription(self, event_type, handler)
|
|
92
|
+
|
|
93
|
+
def unsubscribe(self, event_type: str, handler: EventHandler) -> None:
|
|
94
|
+
"""Remove subscriber handler from active pattern list."""
|
|
95
|
+
if event_type in self._subscribers and handler in self._subscribers[event_type]:
|
|
96
|
+
self._subscribers[event_type].remove(handler)
|
|
97
|
+
if not self._subscribers[event_type]:
|
|
98
|
+
del self._subscribers[event_type]
|
|
99
|
+
logger.debug("Unsubscribed from pattern '%s'", event_type)
|
|
100
|
+
|
|
101
|
+
async def emit_and_wait(self, event: Event, timeout: float = 5.0) -> Event:
|
|
102
|
+
"""Emits an event and suspends execution until a correlating event returns."""
|
|
103
|
+
future: asyncio.Future[Event] = asyncio.get_running_loop().create_future()
|
|
104
|
+
self._pending_responses[event.event_id] = future
|
|
105
|
+
|
|
106
|
+
await self.emit(event)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
response = await asyncio.wait_for(future, timeout=timeout)
|
|
110
|
+
return response
|
|
111
|
+
except TimeoutError:
|
|
112
|
+
logger.error("Timeout waiting for correlation event to emitted ID: %s", event.event_id)
|
|
113
|
+
raise TimeoutError(f"Event correlation timeout for {event.event_id}")
|
|
114
|
+
finally:
|
|
115
|
+
self._pending_responses.pop(event.event_id, None)
|
|
116
|
+
|
|
117
|
+
async def replay(self, from_timestamp: datetime) -> AsyncIterator[Event]:
|
|
118
|
+
"""Asynchronously stream events in history since from_timestamp."""
|
|
119
|
+
target_timestamp = from_timestamp.timestamp()
|
|
120
|
+
filtered_events = [evt for evt in self._history if evt.timestamp >= target_timestamp]
|
|
121
|
+
|
|
122
|
+
sorted_events = sorted(filtered_events, key=lambda x: x.timestamp)
|
|
123
|
+
for event in sorted_events:
|
|
124
|
+
yield event
|
|
125
|
+
|
|
126
|
+
async def start(self) -> None:
|
|
127
|
+
"""Start the background event dispatch loop."""
|
|
128
|
+
if self._running:
|
|
129
|
+
return
|
|
130
|
+
self._running = True
|
|
131
|
+
self._dispatch_task = asyncio.create_task(self._dispatch_loop())
|
|
132
|
+
logger.info("Kernel Event Bus started.")
|
|
133
|
+
|
|
134
|
+
async def stop(self) -> None:
|
|
135
|
+
"""Flush the queue and terminate the dispatch loop."""
|
|
136
|
+
self._running = False
|
|
137
|
+
if self._dispatch_task:
|
|
138
|
+
self._dispatch_task.cancel()
|
|
139
|
+
try:
|
|
140
|
+
await self._dispatch_task
|
|
141
|
+
except asyncio.CancelledError:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
while not self._queue.empty():
|
|
145
|
+
try:
|
|
146
|
+
event = self._queue.get_nowait()
|
|
147
|
+
await self._dispatch_immediate(event)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.error("Error processing queue flush event: %s", e)
|
|
150
|
+
|
|
151
|
+
logger.info("Kernel Event Bus stopped.")
|
|
152
|
+
|
|
153
|
+
async def _dispatch_loop(self) -> None:
|
|
154
|
+
"""Background loop reading and routing events from the queue."""
|
|
155
|
+
while self._running:
|
|
156
|
+
try:
|
|
157
|
+
event = await self._queue.get()
|
|
158
|
+
await self._dispatch_immediate(event)
|
|
159
|
+
self._queue.task_done()
|
|
160
|
+
except asyncio.CancelledError:
|
|
161
|
+
break
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.error("Error in event bus dispatch loop: %s", e)
|
|
164
|
+
|
|
165
|
+
async def _dispatch_immediate(self, event: Event) -> None:
|
|
166
|
+
"""Match event type and invoke all registered subscriber handlers."""
|
|
167
|
+
handlers_to_run = self._find_matching_handlers(event.event_type)
|
|
168
|
+
if not handlers_to_run:
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
tasks = []
|
|
172
|
+
for handler in handlers_to_run:
|
|
173
|
+
try:
|
|
174
|
+
if asyncio.iscoroutinefunction(handler):
|
|
175
|
+
tasks.append(self._run_async_handler(handler, event))
|
|
176
|
+
else:
|
|
177
|
+
handler(event)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error("Error running synchronous event handler: %s", e)
|
|
180
|
+
|
|
181
|
+
if tasks:
|
|
182
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
183
|
+
|
|
184
|
+
async def _run_async_handler(self, handler: EventHandler, event: Event) -> None:
|
|
185
|
+
"""Helper to run a coroutine handler with clean error capture."""
|
|
186
|
+
try:
|
|
187
|
+
await handler(event)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error("Error running asynchronous event handler: %s", e)
|
|
190
|
+
|
|
191
|
+
def _find_matching_handlers(self, event_type: str) -> list[EventHandler]:
|
|
192
|
+
"""Find all subscriber handlers whose pattern matches the event_type."""
|
|
193
|
+
matched: list[EventHandler] = []
|
|
194
|
+
for pattern, handlers in self._subscribers.items():
|
|
195
|
+
if fnmatch.fnmatchcase(event_type, pattern):
|
|
196
|
+
matched.extend(handlers)
|
|
197
|
+
return matched
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# Maintain legacy class name alias as fallback compatibility
|
|
201
|
+
class EventBus(CognitiveBus):
|
|
202
|
+
"""Fallback alias for compatibility with existing modules."""
|
|
203
|
+
|
|
204
|
+
pass
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Velune Isolated Sandbox, Execution DAG Planner, and Rollback Subsystem."""
|
|
2
|
+
|
|
3
|
+
from velune.execution.checkpointer import FileCheckpointer
|
|
4
|
+
from velune.execution.command_spec import CommandSpec
|
|
5
|
+
from velune.execution.executor import ExecutionExecutor
|
|
6
|
+
from velune.execution.planner import ExecutionDAG, ExecutionPlanner
|
|
7
|
+
from velune.execution.rollback import RollbackManager
|
|
8
|
+
from velune.execution.sandbox import SandboxResult, SubprocessSandbox
|
|
9
|
+
from velune.execution.validator import PostExecutionValidator, ValidationResult
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"SubprocessSandbox",
|
|
13
|
+
"SandboxResult",
|
|
14
|
+
"CommandSpec",
|
|
15
|
+
"FileCheckpointer",
|
|
16
|
+
"RollbackManager",
|
|
17
|
+
"PostExecutionValidator",
|
|
18
|
+
"ValidationResult",
|
|
19
|
+
"ExecutionDAG",
|
|
20
|
+
"ExecutionPlanner",
|
|
21
|
+
"ExecutionExecutor",
|
|
22
|
+
]
|