velune-cli 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. velune/__init__.py +5 -0
  2. velune/__main__.py +6 -0
  3. velune/cli/__init__.py +5 -0
  4. velune/cli/app.py +208 -0
  5. velune/cli/autocomplete.py +80 -0
  6. velune/cli/banner.py +60 -0
  7. velune/cli/commands/__init__.py +32 -0
  8. velune/cli/commands/ask.py +175 -0
  9. velune/cli/commands/base.py +16 -0
  10. velune/cli/commands/chat.py +228 -0
  11. velune/cli/commands/config.py +224 -0
  12. velune/cli/commands/daemon.py +88 -0
  13. velune/cli/commands/doctor.py +721 -0
  14. velune/cli/commands/init.py +170 -0
  15. velune/cli/commands/mcp.py +82 -0
  16. velune/cli/commands/memory.py +293 -0
  17. velune/cli/commands/models.py +683 -0
  18. velune/cli/commands/preflight.py +95 -0
  19. velune/cli/commands/run.py +270 -0
  20. velune/cli/commands/setup.py +184 -0
  21. velune/cli/commands/workspace.py +249 -0
  22. velune/cli/context.py +36 -0
  23. velune/cli/councilmodel_ui.py +199 -0
  24. velune/cli/display/council_view.py +254 -0
  25. velune/cli/display/memory_view.py +126 -0
  26. velune/cli/display/panels.py +35 -0
  27. velune/cli/display/progress.py +25 -0
  28. velune/cli/display/themes.py +25 -0
  29. velune/cli/main.py +15 -0
  30. velune/cli/model_selector.py +51 -0
  31. velune/cli/modes.py +86 -0
  32. velune/cli/pull_ui.py +123 -0
  33. velune/cli/registry.py +80 -0
  34. velune/cli/rendering/__init__.py +5 -0
  35. velune/cli/rendering/error_panel.py +79 -0
  36. velune/cli/rendering/markdown.py +63 -0
  37. velune/cli/repl.py +1855 -0
  38. velune/cli/session_manager.py +71 -0
  39. velune/cli/slash_commands.py +37 -0
  40. velune/cli/theme.py +8 -0
  41. velune/cognition/__init__.py +23 -0
  42. velune/cognition/agents/__init__.py +7 -0
  43. velune/cognition/agents/coder.py +209 -0
  44. velune/cognition/agents/planner.py +156 -0
  45. velune/cognition/agents/reviewer.py +195 -0
  46. velune/cognition/arbitrator.py +220 -0
  47. velune/cognition/architecture.py +415 -0
  48. velune/cognition/budget.py +65 -0
  49. velune/cognition/council/__init__.py +47 -0
  50. velune/cognition/council/base.py +217 -0
  51. velune/cognition/council/challenger.py +74 -0
  52. velune/cognition/council/coder.py +79 -0
  53. velune/cognition/council/critic_agent.py +43 -0
  54. velune/cognition/council/critic_configs.py +111 -0
  55. velune/cognition/council/critics.py +41 -0
  56. velune/cognition/council/debate.py +46 -0
  57. velune/cognition/council/factory.py +140 -0
  58. velune/cognition/council/messages.py +56 -0
  59. velune/cognition/council/planner.py +124 -0
  60. velune/cognition/council/reviewer.py +74 -0
  61. velune/cognition/council/synthesizer.py +67 -0
  62. velune/cognition/council/tiers.py +188 -0
  63. velune/cognition/council_orchestrator.py +282 -0
  64. velune/cognition/firewall.py +354 -0
  65. velune/cognition/module.py +46 -0
  66. velune/cognition/orchestrator.py +1205 -0
  67. velune/cognition/personality.py +238 -0
  68. velune/cognition/state.py +104 -0
  69. velune/cognition/style_resolver.py +64 -0
  70. velune/cognition/verification.py +205 -0
  71. velune/context/__init__.py +28 -0
  72. velune/context/assembler.py +240 -0
  73. velune/context/budget.py +97 -0
  74. velune/context/extractive.py +95 -0
  75. velune/context/prompt_adaptation.py +480 -0
  76. velune/context/sections.py +99 -0
  77. velune/context/token_counter.py +134 -0
  78. velune/context/utilization.py +33 -0
  79. velune/context/window.py +63 -0
  80. velune/core/__init__.py +89 -0
  81. velune/core/background.py +5 -0
  82. velune/core/config/__init__.py +37 -0
  83. velune/core/errors/__init__.py +90 -0
  84. velune/core/errors/catalog.py +188 -0
  85. velune/core/errors/execution.py +31 -0
  86. velune/core/errors/memory.py +25 -0
  87. velune/core/errors/orchestration.py +31 -0
  88. velune/core/errors/provider.py +37 -0
  89. velune/core/event_loop.py +35 -0
  90. velune/core/logging.py +83 -0
  91. velune/core/paths.py +165 -0
  92. velune/core/runtime.py +113 -0
  93. velune/core/startup_profiler.py +56 -0
  94. velune/core/task_registry.py +117 -0
  95. velune/core/trace.py +83 -0
  96. velune/core/types/__init__.py +48 -0
  97. velune/core/types/agent.py +53 -0
  98. velune/core/types/context.py +42 -0
  99. velune/core/types/inference.py +38 -0
  100. velune/core/types/memory.py +42 -0
  101. velune/core/types/model.py +70 -0
  102. velune/core/types/provider.py +62 -0
  103. velune/core/types/repository.py +38 -0
  104. velune/core/types/task.py +61 -0
  105. velune/core/types/workspace.py +28 -0
  106. velune/daemon/client.py +13 -0
  107. velune/daemon/server.py +127 -0
  108. velune/daemon/transport.py +179 -0
  109. velune/events.py +204 -0
  110. velune/execution/__init__.py +22 -0
  111. velune/execution/benchmarker.py +315 -0
  112. velune/execution/cancellation.py +53 -0
  113. velune/execution/checkpointer.py +130 -0
  114. velune/execution/command_spec.py +165 -0
  115. velune/execution/diff_preview.py +197 -0
  116. velune/execution/executor.py +181 -0
  117. velune/execution/module.py +18 -0
  118. velune/execution/multi_diff.py +67 -0
  119. velune/execution/path_guard.py +74 -0
  120. velune/execution/planner.py +91 -0
  121. velune/execution/rollback.py +89 -0
  122. velune/execution/sandbox.py +268 -0
  123. velune/execution/validator.py +115 -0
  124. velune/hardware/__init__.py +1 -0
  125. velune/hardware/detector.py +192 -0
  126. velune/kernel/__init__.py +55 -0
  127. velune/kernel/bootstrap.py +125 -0
  128. velune/kernel/config.py +426 -0
  129. velune/kernel/entrypoint.py +78 -0
  130. velune/kernel/health.py +54 -0
  131. velune/kernel/lifecycle.py +143 -0
  132. velune/kernel/module.py +17 -0
  133. velune/kernel/modules.py +23 -0
  134. velune/kernel/registry.py +96 -0
  135. velune/kernel/schemas.py +28 -0
  136. velune/main.py +9 -0
  137. velune/mcp/__init__.py +9 -0
  138. velune/mcp/client.py +115 -0
  139. velune/mcp/config.py +19 -0
  140. velune/mcp/server.py +624 -0
  141. velune/memory/__init__.py +32 -0
  142. velune/memory/compaction.py +506 -0
  143. velune/memory/embedding_pipeline.py +241 -0
  144. velune/memory/lifecycle.py +680 -0
  145. velune/memory/module.py +218 -0
  146. velune/memory/prioritizer.py +67 -0
  147. velune/memory/storage/episodic_schema.sql +53 -0
  148. velune/memory/storage/lancedb_store.py +282 -0
  149. velune/memory/storage/sqlite_manager.py +369 -0
  150. velune/memory/storage/sqlite_pool.py +149 -0
  151. velune/memory/tiers/episodic.py +588 -0
  152. velune/memory/tiers/graph.py +378 -0
  153. velune/memory/tiers/lineage.py +416 -0
  154. velune/memory/tiers/semantic.py +475 -0
  155. velune/memory/tiers/working.py +168 -0
  156. velune/memory/vitality.py +132 -0
  157. velune/models/__init__.py +15 -0
  158. velune/models/family.py +76 -0
  159. velune/models/module.py +20 -0
  160. velune/models/probes.py +192 -0
  161. velune/models/profile_cache.py +84 -0
  162. velune/models/profiler.py +108 -0
  163. velune/models/registry.py +251 -0
  164. velune/models/scorer.py +233 -0
  165. velune/models/specializations.py +205 -0
  166. velune/orchestration/__init__.py +19 -0
  167. velune/orchestration/engine.py +239 -0
  168. velune/orchestration/module.py +15 -0
  169. velune/orchestration/role_assignments.py +82 -0
  170. velune/orchestration/schemas.py +98 -0
  171. velune/plugins/__init__.py +20 -0
  172. velune/plugins/hooks.py +50 -0
  173. velune/plugins/loader.py +161 -0
  174. velune/plugins/registry.py +56 -0
  175. velune/plugins/schemas.py +21 -0
  176. velune/providers/__init__.py +23 -0
  177. velune/providers/adapters/anthropic.py +257 -0
  178. velune/providers/adapters/fireworks.py +115 -0
  179. velune/providers/adapters/google.py +234 -0
  180. velune/providers/adapters/groq.py +151 -0
  181. velune/providers/adapters/huggingface.py +210 -0
  182. velune/providers/adapters/llamacpp.py +208 -0
  183. velune/providers/adapters/lmstudio.py +175 -0
  184. velune/providers/adapters/ollama.py +233 -0
  185. velune/providers/adapters/openai.py +213 -0
  186. velune/providers/adapters/openrouter.py +81 -0
  187. velune/providers/adapters/together.py +134 -0
  188. velune/providers/adapters/xai.py +60 -0
  189. velune/providers/base.py +86 -0
  190. velune/providers/benchmarker.py +138 -0
  191. velune/providers/discovery/__init__.py +33 -0
  192. velune/providers/discovery/anthropic.py +79 -0
  193. velune/providers/discovery/benchmarks.py +44 -0
  194. velune/providers/discovery/classifier.py +69 -0
  195. velune/providers/discovery/fireworks.py +95 -0
  196. velune/providers/discovery/gguf.py +88 -0
  197. velune/providers/discovery/google.py +95 -0
  198. velune/providers/discovery/gpu.py +117 -0
  199. velune/providers/discovery/groq.py +21 -0
  200. velune/providers/discovery/huggingface.py +67 -0
  201. velune/providers/discovery/lmstudio.py +80 -0
  202. velune/providers/discovery/ollama.py +162 -0
  203. velune/providers/discovery/openai.py +96 -0
  204. velune/providers/discovery/openrouter.py +113 -0
  205. velune/providers/discovery/scanner.py +115 -0
  206. velune/providers/discovery/together.py +114 -0
  207. velune/providers/discovery/xai.py +57 -0
  208. velune/providers/health.py +67 -0
  209. velune/providers/health_monitor.py +169 -0
  210. velune/providers/keystore.py +142 -0
  211. velune/providers/local_paths.py +49 -0
  212. velune/providers/local_resolver.py +229 -0
  213. velune/providers/module.py +51 -0
  214. velune/providers/ollama_manager.py +193 -0
  215. velune/providers/registry.py +220 -0
  216. velune/providers/router.py +255 -0
  217. velune/providers/task_classifier.py +288 -0
  218. velune/py.typed +0 -0
  219. velune/repository/__init__.py +33 -0
  220. velune/repository/analyzer.py +127 -0
  221. velune/repository/ast_parser.py +822 -0
  222. velune/repository/blast_radius.py +298 -0
  223. velune/repository/boundary_classifier.py +295 -0
  224. velune/repository/cognition.py +316 -0
  225. velune/repository/grapher.py +179 -0
  226. velune/repository/import_graph.py +263 -0
  227. velune/repository/incremental_indexer.py +275 -0
  228. velune/repository/index_state.py +96 -0
  229. velune/repository/indexer.py +243 -0
  230. velune/repository/module.py +17 -0
  231. velune/repository/parser.py +474 -0
  232. velune/repository/project_type.py +300 -0
  233. velune/repository/rename_journal.py +287 -0
  234. velune/repository/scanner.py +193 -0
  235. velune/repository/schemas.py +102 -0
  236. velune/repository/symbol_registry.py +365 -0
  237. velune/repository/tracker.py +252 -0
  238. velune/retrieval/__init__.py +27 -0
  239. velune/retrieval/cache.py +110 -0
  240. velune/retrieval/fast_path.py +391 -0
  241. velune/retrieval/graph.py +124 -0
  242. velune/retrieval/hybrid.py +271 -0
  243. velune/retrieval/keyword.py +131 -0
  244. velune/retrieval/module.py +26 -0
  245. velune/retrieval/pipeline.py +303 -0
  246. velune/retrieval/reranker.py +102 -0
  247. velune/retrieval/schemas.py +59 -0
  248. velune/retrieval/slow_path.py +364 -0
  249. velune/retrieval/vector.py +203 -0
  250. velune/telemetry/__init__.py +59 -0
  251. velune/telemetry/cognition.py +267 -0
  252. velune/telemetry/cost_estimator.py +92 -0
  253. velune/telemetry/debug.py +304 -0
  254. velune/telemetry/doctor.py +244 -0
  255. velune/telemetry/logging.py +286 -0
  256. velune/telemetry/spans.py +277 -0
  257. velune/telemetry/token_tracker.py +140 -0
  258. velune/telemetry/usage_tracker.py +340 -0
  259. velune/tools/__init__.py +41 -0
  260. velune/tools/base/registry.py +87 -0
  261. velune/tools/base/tool.py +63 -0
  262. velune/tools/code/navigate.py +116 -0
  263. velune/tools/code/search.py +123 -0
  264. velune/tools/filesystem/read.py +75 -0
  265. velune/tools/filesystem/search.py +136 -0
  266. velune/tools/filesystem/write.py +163 -0
  267. velune/tools/git/history.py +177 -0
  268. velune/tools/git/operations.py +122 -0
  269. velune/tools/git/state.py +121 -0
  270. velune/tools/module.py +81 -0
  271. velune/tools/terminal/execute.py +72 -0
  272. velune/tools/terminal/history.py +47 -0
  273. velune/tools/web/fetch.py +55 -0
  274. velune/tools/web/validator.py +122 -0
  275. velune_cli-0.9.0.dist-info/METADATA +518 -0
  276. velune_cli-0.9.0.dist-info/RECORD +279 -0
  277. velune_cli-0.9.0.dist-info/WHEEL +4 -0
  278. velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
  279. velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,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
+ ]