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,218 @@
1
+ from velune.kernel.bootstrap import RuntimeEnvironment, SubsystemModule
2
+
3
+
4
+ def _create_sqlite_pool(env: RuntimeEnvironment):
5
+ from velune.core.paths import cognitive_db_path, migrate_legacy_storage
6
+ from velune.memory.storage.sqlite_pool import SQLiteConnectionPool
7
+
8
+ # One-time relocation of any pre-existing in-workspace (cloud-synced) data.
9
+ migrate_legacy_storage(env.workspace)
10
+ pool = SQLiteConnectionPool(cognitive_db_path(env.workspace))
11
+ # Register is_healthy as a custom health hook when the monitor is available.
12
+ if env.container.has("runtime.health_monitor"):
13
+ monitor = env.container.get("runtime.health_monitor")
14
+ monitor.register_health_hook("sqlite_pool", pool.health_check)
15
+ return pool
16
+
17
+
18
+ def _create_working_tier(env: RuntimeEnvironment):
19
+ from velune.memory.tiers.working import WorkingMemoryTier
20
+
21
+ return WorkingMemoryTier()
22
+
23
+
24
+ def _create_episodic_tier(env: RuntimeEnvironment):
25
+ from velune.memory.tiers.episodic import EpisodicMemoryTier
26
+
27
+ pool = env.container.get("runtime.sqlite_pool")
28
+ return EpisodicMemoryTier(pool)
29
+
30
+
31
+ def _create_semantic_tier(env: RuntimeEnvironment):
32
+ from velune.core.paths import qdrant_store_path
33
+ from velune.memory.tiers.semantic import SemanticMemoryTier
34
+
35
+ return SemanticMemoryTier(path=str(qdrant_store_path(env.workspace)))
36
+
37
+
38
+ def _create_graph_tier(env: RuntimeEnvironment):
39
+ from velune.memory.tiers.graph import GraphMemoryTier
40
+
41
+ pool = env.container.get("runtime.sqlite_pool")
42
+ return GraphMemoryTier(pool)
43
+
44
+
45
+ def _create_lineage_tier(env: RuntimeEnvironment):
46
+ from velune.memory.tiers.lineage import LineageMemoryTier
47
+
48
+ pool = env.container.get("runtime.sqlite_pool")
49
+ return LineageMemoryTier(pool)
50
+
51
+
52
+ def _create_lancedb_store(env: RuntimeEnvironment):
53
+ from velune.core.paths import lancedb_store_path
54
+ from velune.memory.storage.lancedb_store import LanceDBStore
55
+
56
+ store = LanceDBStore(lancedb_store_path(env.workspace))
57
+ if env.container.has("runtime.health_monitor"):
58
+ monitor = env.container.get("runtime.health_monitor")
59
+ monitor.register_health_hook("lancedb_store", store.health_check)
60
+ return store
61
+
62
+
63
+ def _create_embedding_pipeline(env: RuntimeEnvironment):
64
+ from velune.memory.embedding_pipeline import EmbeddingPipeline
65
+
66
+ store = env.container.get("runtime.lancedb_store")
67
+ try:
68
+ provider_registry = env.container.get("runtime.provider_registry")
69
+ provider = provider_registry.get("ollama") if provider_registry else None
70
+ except Exception:
71
+ provider = None
72
+ return EmbeddingPipeline(provider, store)
73
+
74
+
75
+ def _create_semantic_memory_lance(env: RuntimeEnvironment):
76
+ from velune.memory.tiers.semantic import SemanticMemory
77
+
78
+ store = env.container.get("runtime.lancedb_store")
79
+ pipeline = env.container.get("runtime.embedding_pipeline")
80
+ return SemanticMemory(store, pipeline)
81
+
82
+
83
+ def _create_episodic_session_memory(env: RuntimeEnvironment):
84
+ from velune.memory.tiers.episodic import EpisodicMemory
85
+
86
+ pool = env.container.get("runtime.sqlite_pool")
87
+ return EpisodicMemory(pool)
88
+
89
+
90
+ def _create_memory_lifecycle(env: RuntimeEnvironment):
91
+ from velune.memory.lifecycle import MemoryLifecycleManager
92
+
93
+ working_tier = env.container.get("runtime.working_memory")
94
+ episodic_tier = env.container.get("runtime.episodic_memory")
95
+ episodic_memory = env.container.get("runtime.episodic_session_memory")
96
+ semantic_memory = env.container.get("runtime.semantic_memory_lance")
97
+ embedding_pipeline = env.container.get("runtime.embedding_pipeline")
98
+ lineage_tier = env.container.get("runtime.lineage_memory")
99
+
100
+ manager = MemoryLifecycleManager(
101
+ working_tier=working_tier,
102
+ episodic_memory=episodic_memory,
103
+ semantic_memory=semantic_memory,
104
+ embedding_pipeline=embedding_pipeline,
105
+ lineage_tier=lineage_tier,
106
+ episodic_session_memory=episodic_tier,
107
+ )
108
+
109
+ # Register health hook if monitor is available
110
+ if env.container.has("runtime.health_monitor"):
111
+ monitor = env.container.get("runtime.health_monitor")
112
+
113
+ async def _memory_health_check() -> dict:
114
+ try:
115
+ health = await manager.health()
116
+ return health.to_dict()
117
+ except Exception as e:
118
+ return {"error": str(e)}
119
+
120
+ # Bridge the async health() coroutine onto the synchronous health-hook
121
+ # contract via submit(), which routes through the single run_async()
122
+ # entry point in velune.kernel.entrypoint. submit() raises RuntimeError
123
+ # when already inside a running event loop; in that case we return a
124
+ # placeholder rather than blocking the loop with a nested run.
125
+ def _sync_health_check() -> dict:
126
+ from velune.core.event_loop import submit
127
+
128
+ try:
129
+ return submit(_memory_health_check())
130
+ except RuntimeError:
131
+ return {"status": "async_only"}
132
+ except Exception as e:
133
+ return {"error": str(e)}
134
+
135
+ monitor.register_health_hook("memory", _sync_health_check)
136
+
137
+ return manager
138
+
139
+
140
+ MEMORY_MODULES = [
141
+ SubsystemModule(
142
+ name="sqlite_pool",
143
+ factory=_create_sqlite_pool,
144
+ container_key="runtime.sqlite_pool",
145
+ lifecycle_key="sqlite_pool", # pool.initialize() → pool.startup()
146
+ ),
147
+ SubsystemModule(
148
+ name="working_memory",
149
+ factory=_create_working_tier,
150
+ container_key="runtime.working_memory",
151
+ ),
152
+ SubsystemModule(
153
+ name="episodic_memory",
154
+ factory=_create_episodic_tier,
155
+ container_key="runtime.episodic_memory",
156
+ lifecycle_key="episodic_memory", # tier.initialize() → _init_db()
157
+ dependencies=["runtime.sqlite_pool"],
158
+ ),
159
+ SubsystemModule(
160
+ name="semantic_memory",
161
+ factory=_create_semantic_tier,
162
+ container_key="runtime.semantic_memory",
163
+ ),
164
+ SubsystemModule(
165
+ name="graph_memory",
166
+ factory=_create_graph_tier,
167
+ container_key="runtime.graph_memory",
168
+ lifecycle_key="graph_memory", # tier.initialize() → _init_db()
169
+ dependencies=["runtime.sqlite_pool"],
170
+ ),
171
+ SubsystemModule(
172
+ name="lineage_memory",
173
+ factory=_create_lineage_tier,
174
+ container_key="runtime.lineage_memory",
175
+ lifecycle_key="lineage_memory", # tier.initialize() → _init_db()
176
+ dependencies=["runtime.sqlite_pool"],
177
+ ),
178
+ SubsystemModule(
179
+ name="lancedb_store",
180
+ factory=_create_lancedb_store,
181
+ container_key="runtime.lancedb_store",
182
+ lifecycle_key="lancedb_store", # calls LanceDBStore.initialize()
183
+ ),
184
+ SubsystemModule(
185
+ name="embedding_pipeline",
186
+ factory=_create_embedding_pipeline,
187
+ container_key="runtime.embedding_pipeline",
188
+ lifecycle_key="embedding_pipeline", # calls EmbeddingPipeline.initialize()
189
+ dependencies=["runtime.lancedb_store"],
190
+ ),
191
+ SubsystemModule(
192
+ name="semantic_memory_lance",
193
+ factory=_create_semantic_memory_lance,
194
+ container_key="runtime.semantic_memory_lance",
195
+ dependencies=["runtime.lancedb_store", "runtime.embedding_pipeline"],
196
+ ),
197
+ SubsystemModule(
198
+ name="episodic_session_memory",
199
+ factory=_create_episodic_session_memory,
200
+ container_key="runtime.episodic_session_memory",
201
+ lifecycle_key="episodic_session_memory", # calls EpisodicMemory.initialize()
202
+ dependencies=["runtime.sqlite_pool"],
203
+ ),
204
+ SubsystemModule(
205
+ name="memory_lifecycle",
206
+ factory=_create_memory_lifecycle,
207
+ container_key="runtime.memory_lifecycle",
208
+ lifecycle_key="memory",
209
+ dependencies=[
210
+ "runtime.working_memory",
211
+ "runtime.episodic_memory",
212
+ "runtime.episodic_session_memory",
213
+ "runtime.semantic_memory_lance",
214
+ "runtime.embedding_pipeline",
215
+ "runtime.lineage_memory",
216
+ ],
217
+ ),
218
+ ]
@@ -0,0 +1,67 @@
1
+ """Memory Prioritization & Decay Engine.
2
+
3
+ Calculates memory importance and decay rates using exponential decay
4
+ models combined with usage-frequency boosts.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ import time
11
+
12
+
13
+ class MemoryPrioritizer:
14
+ """Manages importance calculations and temporal decay profiles for cognitive memories."""
15
+
16
+ def __init__(
17
+ self,
18
+ default_halflife_hours: float = 24.0,
19
+ retrieval_boost_factor: float = 0.15,
20
+ max_importance: float = 1.0,
21
+ ) -> None:
22
+ """
23
+ Initialize the prioritizer.
24
+
25
+ - default_halflife_hours: Hours for memory importance to decay by half.
26
+ - retrieval_boost_factor: Percentage boost added to importance on access.
27
+ """
28
+ self.default_halflife_hours = default_halflife_hours
29
+ self.retrieval_boost_factor = retrieval_boost_factor
30
+ self.max_importance = max_importance
31
+
32
+ # Compute decay constant (lambda = ln(2) / halflife)
33
+ # Convert halflife from hours to seconds
34
+ self.decay_constant = math.log(2.0) / (default_halflife_hours * 3600.0)
35
+
36
+ def calculate_decayed_score(self, initial_score: float, creation_timestamp: float) -> float:
37
+ """
38
+ Applies exponential decay over elapsed time.
39
+
40
+ S(t) = S_0 * e^(-lambda * t)
41
+ """
42
+ elapsed_seconds = max(0.0, time.time() - creation_timestamp)
43
+ decayed_score = initial_score * math.exp(-self.decay_constant * elapsed_seconds)
44
+ return max(0.0, min(self.max_importance, decayed_score))
45
+
46
+ def calculate_initial_importance(
47
+ self,
48
+ base_importance: float,
49
+ semantic_depth: float = 0.5,
50
+ context_fit: float = 0.5,
51
+ ) -> float:
52
+ """
53
+ Combines baseline importance with dynamic context factors.
54
+
55
+ Initial = 0.5 * base + 0.3 * semantic_depth + 0.2 * context_fit
56
+ """
57
+ score = (0.5 * base_importance) + (0.3 * semantic_depth) + (0.2 * context_fit)
58
+ return max(0.0, min(self.max_importance, score))
59
+
60
+ def apply_retrieval_boost(self, current_score: float) -> float:
61
+ """
62
+ Apply a retrieval boost when a node is fetched/replayed.
63
+ """
64
+ boosted_score = current_score + (
65
+ self.retrieval_boost_factor * (self.max_importance - current_score)
66
+ )
67
+ return max(0.0, min(self.max_importance, boosted_score))
@@ -0,0 +1,53 @@
1
+ -- Episodic Memory Schema — Phase 2a (storage layer)
2
+ -- Applied incrementally by EpisodicMemory._apply_migrations().
3
+ -- The Python class owns migration execution; this file is the canonical reference.
4
+
5
+ -- ── Schema versioning ────────────────────────────────────────────────────────
6
+ CREATE TABLE IF NOT EXISTS episodic_schema_version (
7
+ version INTEGER PRIMARY KEY,
8
+ applied_at REAL NOT NULL
9
+ );
10
+
11
+ -- ── Version 1 ────────────────────────────────────────────────────────────────
12
+
13
+ -- One row per REPL session (from start_session() to end_session()).
14
+ CREATE TABLE IF NOT EXISTS sessions (
15
+ id TEXT PRIMARY KEY,
16
+ workspace_root TEXT NOT NULL,
17
+ started_at REAL NOT NULL,
18
+ ended_at REAL, -- NULL while session is active
19
+ model_used TEXT,
20
+ mode TEXT,
21
+ total_tokens INTEGER DEFAULT 0,
22
+ summary TEXT -- LLM-generated; added at session end
23
+ );
24
+
25
+ -- One row per conversation turn (user or assistant message).
26
+ CREATE TABLE IF NOT EXISTS turns (
27
+ id TEXT PRIMARY KEY,
28
+ session_id TEXT NOT NULL REFERENCES sessions(id),
29
+ turn_index INTEGER NOT NULL, -- 0-based position within session
30
+ role TEXT NOT NULL, -- 'user' or 'assistant'
31
+ content TEXT NOT NULL,
32
+ model_used TEXT,
33
+ tokens_used INTEGER,
34
+ created_at REAL NOT NULL,
35
+ embedding_id TEXT -- set after embedding in Phase 2a-2
36
+ );
37
+
38
+ -- Structured annotations on turns (e.g. 'architectural_decision', 'bug_fix').
39
+ CREATE TABLE IF NOT EXISTS memory_tags (
40
+ turn_id TEXT NOT NULL REFERENCES turns(id),
41
+ tag TEXT NOT NULL,
42
+ value TEXT, -- optional structured payload
43
+ PRIMARY KEY (turn_id, tag)
44
+ );
45
+
46
+ -- ── Indexes ───────────────────────────────────────────────────────────────────
47
+ CREATE INDEX IF NOT EXISTS idx_sessions_workspace ON sessions(workspace_root);
48
+ CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
49
+ CREATE INDEX IF NOT EXISTS idx_turns_session ON turns(session_id);
50
+ CREATE INDEX IF NOT EXISTS idx_turns_created ON turns(created_at);
51
+ CREATE INDEX IF NOT EXISTS idx_turns_role ON turns(role);
52
+ CREATE INDEX IF NOT EXISTS idx_tags_turn ON memory_tags(turn_id);
53
+ CREATE INDEX IF NOT EXISTS idx_tags_tag ON memory_tags(tag);
@@ -0,0 +1,282 @@
1
+ """LanceDB-backed vector store for semantic memory (Phase 2a).
2
+
3
+ LanceDB is an embedded, serverless vector database — no daemon process is
4
+ needed. All heavy operations (open, add, search, delete) are pushed to a
5
+ thread pool via ``asyncio.to_thread`` so they never block the event loop.
6
+
7
+ Graceful degradation: if LanceDB or PyArrow is not installed, or if startup
8
+ fails, ``_degraded`` is set to True and all operations become safe no-ops.
9
+ Set ``VELUNE_SKIP_LANCEDB=1`` to force degraded mode for fast dev iteration.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import logging
16
+ import os
17
+ import time
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ logger = logging.getLogger("velune.memory.storage.lancedb_store")
23
+
24
+ # Default embedding dimension for nomic-embed-text via Ollama
25
+ EMBEDDING_DIM: int = 768
26
+
27
+ _TABLE_NAME = "memories"
28
+
29
+
30
+ # ── Data models ───────────────────────────────────────────────────────────────
31
+
32
+
33
+ @dataclass
34
+ class MemoryRecord:
35
+ """A vector record to upsert into LanceDB."""
36
+
37
+ id: str
38
+ embedding: list[float]
39
+ content: str
40
+ source_type: str # "turn_user", "turn_assistant", "session_summary"
41
+ session_id: str
42
+ turn_id: str
43
+ workspace_root: str
44
+ created_at: float
45
+ trust_score: float = 1.0
46
+
47
+
48
+ @dataclass
49
+ class SearchResult:
50
+ """A single result returned from a vector similarity search."""
51
+
52
+ id: str
53
+ content: str
54
+ source_type: str
55
+ distance: float
56
+ trust_score: float
57
+ session_id: str
58
+ turn_id: str
59
+ created_at: float = field(default_factory=time.time)
60
+
61
+
62
+ # ── Store ─────────────────────────────────────────────────────────────────────
63
+
64
+
65
+ class LanceDBStore:
66
+ """Async wrapper around a local LanceDB table for semantic memory storage.
67
+
68
+ Call ``await store.startup()`` (or ``await store.initialize()``) before
69
+ any read/write operations; the module lifecycle does this automatically.
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ store_path: Path,
75
+ embedding_dim: int = EMBEDDING_DIM,
76
+ ) -> None:
77
+ self._store_path = store_path
78
+ self._embedding_dim = embedding_dim
79
+ self._db: Any = None
80
+ self._table: Any = None
81
+ self._degraded: bool = os.environ.get("VELUNE_SKIP_LANCEDB", "").lower() in (
82
+ "1",
83
+ "true",
84
+ "yes",
85
+ )
86
+ if self._degraded:
87
+ logger.warning("VELUNE_SKIP_LANCEDB set — LanceDB running in degraded mode.")
88
+
89
+ # ── Lifecycle ─────────────────────────────────────────────────────────────
90
+
91
+ async def startup(self) -> None:
92
+ """Open (or create) the LanceDB store and the memories table."""
93
+ if self._degraded:
94
+ return
95
+ self._store_path.mkdir(parents=True, exist_ok=True)
96
+ try:
97
+ await asyncio.to_thread(self._open_or_create)
98
+ logger.info("LanceDB store ready at %s (%d-dim)", self._store_path, self._embedding_dim)
99
+ except Exception as exc:
100
+ logger.error("LanceDB startup failed; falling back to degraded mode: %s", exc)
101
+ self._degraded = True
102
+
103
+ async def initialize(self) -> None:
104
+ """Lifecycle alias called by the module coordinator."""
105
+ await self.startup()
106
+
107
+ async def shutdown(self) -> None:
108
+ self._db = None
109
+ self._table = None
110
+
111
+ def _open_or_create(self) -> None:
112
+ import lancedb
113
+ import pyarrow as pa
114
+
115
+ self._db = lancedb.connect(str(self._store_path))
116
+ existing = self._db.table_names()
117
+
118
+ if _TABLE_NAME in existing:
119
+ self._table = self._db.open_table(_TABLE_NAME)
120
+ logger.debug("Opened existing LanceDB table '%s'", _TABLE_NAME)
121
+ else:
122
+ schema = pa.schema(
123
+ [
124
+ pa.field("id", pa.utf8()),
125
+ pa.field("embedding", pa.list_(pa.float32(), self._embedding_dim)),
126
+ pa.field("content", pa.utf8()),
127
+ pa.field("source_type", pa.utf8()),
128
+ pa.field("session_id", pa.utf8()),
129
+ pa.field("turn_id", pa.utf8()),
130
+ pa.field("workspace_root", pa.utf8()),
131
+ pa.field("created_at", pa.float64()),
132
+ pa.field("trust_score", pa.float32()),
133
+ ]
134
+ )
135
+ self._table = self._db.create_table(_TABLE_NAME, schema=schema)
136
+ logger.info("Created LanceDB table '%s'", _TABLE_NAME)
137
+
138
+ # ── Write operations ──────────────────────────────────────────────────────
139
+
140
+ async def upsert(self, records: list[MemoryRecord]) -> None:
141
+ """Delete any existing records with matching IDs, then insert fresh copies."""
142
+ if self._degraded or not records or self._table is None:
143
+ return
144
+ try:
145
+ await asyncio.to_thread(self._upsert_sync, records)
146
+ except Exception as exc:
147
+ logger.error("LanceDB upsert failed: %s", exc)
148
+
149
+ def _upsert_sync(self, records: list[MemoryRecord]) -> None:
150
+ ids = [r.id for r in records]
151
+ ids_clause = ", ".join(f"'{_esc(i)}'" for i in ids)
152
+ # Best-effort delete; ignore errors on an empty table
153
+ try:
154
+ self._table.delete(f"id IN ({ids_clause})")
155
+ except Exception:
156
+ pass
157
+
158
+ rows = [
159
+ {
160
+ "id": r.id,
161
+ "embedding": [float(x) for x in r.embedding],
162
+ "content": r.content,
163
+ "source_type": r.source_type,
164
+ "session_id": r.session_id,
165
+ "turn_id": r.turn_id,
166
+ "workspace_root": r.workspace_root,
167
+ "created_at": float(r.created_at),
168
+ "trust_score": float(r.trust_score),
169
+ }
170
+ for r in records
171
+ ]
172
+ self._table.add(rows)
173
+
174
+ async def delete(self, ids: list[str]) -> None:
175
+ """Delete records by their string IDs."""
176
+ if self._degraded or not ids or self._table is None:
177
+ return
178
+ try:
179
+ await asyncio.to_thread(self._delete_sync, ids)
180
+ except Exception as exc:
181
+ logger.error("LanceDB delete failed: %s", exc)
182
+
183
+ def _delete_sync(self, ids: list[str]) -> None:
184
+ ids_clause = ", ".join(f"'{_esc(i)}'" for i in ids)
185
+ self._table.delete(f"id IN ({ids_clause})")
186
+
187
+ async def prune_by_trust(self, threshold: float) -> int:
188
+ """Delete records whose trust_score falls below *threshold*. Returns count deleted."""
189
+ if self._degraded or self._table is None:
190
+ return 0
191
+ try:
192
+ return await asyncio.to_thread(self._prune_sync, threshold)
193
+ except Exception as exc:
194
+ logger.error("LanceDB prune failed: %s", exc)
195
+ return 0
196
+
197
+ def _prune_sync(self, threshold: float) -> int:
198
+ before = len(self._table)
199
+ self._table.delete(f"trust_score < {threshold}")
200
+ after = len(self._table)
201
+ return max(0, before - after)
202
+
203
+ # ── Search ─────────────────────────────────────────────────────────────────
204
+
205
+ async def search(
206
+ self,
207
+ embedding: list[float],
208
+ limit: int = 10,
209
+ workspace_root: str | None = None,
210
+ ) -> list[SearchResult]:
211
+ """ANN search over the memories table, optionally pre-filtered by workspace."""
212
+ if self._degraded or self._table is None:
213
+ return []
214
+ try:
215
+ return await asyncio.to_thread(self._search_sync, embedding, limit, workspace_root)
216
+ except Exception as exc:
217
+ logger.error("LanceDB search failed: %s", exc)
218
+ return []
219
+
220
+ def _search_sync(
221
+ self,
222
+ embedding: list[float],
223
+ limit: int,
224
+ workspace_root: str | None,
225
+ ) -> list[SearchResult]:
226
+ import numpy as np
227
+
228
+ query = np.array(embedding, dtype=np.float32)
229
+ q = self._table.search(query, vector_column_name="embedding")
230
+
231
+ if workspace_root:
232
+ safe = _esc(workspace_root)
233
+ q = q.where(f"workspace_root = '{safe}'", prefilter=True)
234
+
235
+ rows = q.limit(limit).to_list()
236
+ results: list[SearchResult] = []
237
+ for row in rows:
238
+ results.append(
239
+ SearchResult(
240
+ id=row["id"],
241
+ content=row["content"],
242
+ source_type=row["source_type"],
243
+ distance=float(row.get("_distance", 0.0)),
244
+ trust_score=float(row["trust_score"]),
245
+ session_id=row["session_id"],
246
+ turn_id=row["turn_id"],
247
+ created_at=float(row.get("created_at", 0.0)),
248
+ )
249
+ )
250
+ return results
251
+
252
+ # ── Properties ────────────────────────────────────────────────────────────
253
+
254
+ @property
255
+ def table_size(self) -> int:
256
+ if self._degraded or self._table is None:
257
+ return 0
258
+ try:
259
+ return len(self._table)
260
+ except Exception:
261
+ return 0
262
+
263
+ @property
264
+ def is_healthy(self) -> bool:
265
+ return not self._degraded and self._table is not None
266
+
267
+ def health_check(self) -> dict:
268
+ return {
269
+ "healthy": self.is_healthy,
270
+ "degraded": self._degraded,
271
+ "store_path": str(self._store_path),
272
+ "embedding_dim": self._embedding_dim,
273
+ "table_size": self.table_size,
274
+ }
275
+
276
+
277
+ # ── Helpers ───────────────────────────────────────────────────────────────────
278
+
279
+
280
+ def _esc(s: str) -> str:
281
+ """Escape single quotes for safe SQL string literals in LanceDB WHERE clauses."""
282
+ return s.replace("'", "''")