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/memory/module.py
ADDED
|
@@ -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("'", "''")
|