velune-cli 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +208 -0
- velune/cli/autocomplete.py +80 -0
- velune/cli/banner.py +60 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +175 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +228 -0
- velune/cli/commands/config.py +224 -0
- velune/cli/commands/daemon.py +88 -0
- velune/cli/commands/doctor.py +721 -0
- velune/cli/commands/init.py +170 -0
- velune/cli/commands/mcp.py +82 -0
- velune/cli/commands/memory.py +293 -0
- velune/cli/commands/models.py +683 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +270 -0
- velune/cli/commands/setup.py +184 -0
- velune/cli/commands/workspace.py +249 -0
- velune/cli/context.py +36 -0
- velune/cli/councilmodel_ui.py +199 -0
- velune/cli/display/council_view.py +254 -0
- velune/cli/display/memory_view.py +126 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +25 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +51 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +123 -0
- velune/cli/registry.py +80 -0
- velune/cli/rendering/__init__.py +5 -0
- velune/cli/rendering/error_panel.py +79 -0
- velune/cli/rendering/markdown.py +63 -0
- velune/cli/repl.py +1855 -0
- velune/cli/session_manager.py +71 -0
- velune/cli/slash_commands.py +37 -0
- velune/cli/theme.py +8 -0
- velune/cognition/__init__.py +23 -0
- velune/cognition/agents/__init__.py +7 -0
- velune/cognition/agents/coder.py +209 -0
- velune/cognition/agents/planner.py +156 -0
- velune/cognition/agents/reviewer.py +195 -0
- velune/cognition/arbitrator.py +220 -0
- velune/cognition/architecture.py +415 -0
- velune/cognition/budget.py +65 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +217 -0
- velune/cognition/council/challenger.py +74 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +43 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +46 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +56 -0
- velune/cognition/council/planner.py +124 -0
- velune/cognition/council/reviewer.py +74 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +188 -0
- velune/cognition/council_orchestrator.py +282 -0
- velune/cognition/firewall.py +354 -0
- velune/cognition/module.py +46 -0
- velune/cognition/orchestrator.py +1205 -0
- velune/cognition/personality.py +238 -0
- velune/cognition/state.py +104 -0
- velune/cognition/style_resolver.py +64 -0
- velune/cognition/verification.py +205 -0
- velune/context/__init__.py +28 -0
- velune/context/assembler.py +240 -0
- velune/context/budget.py +97 -0
- velune/context/extractive.py +95 -0
- velune/context/prompt_adaptation.py +480 -0
- velune/context/sections.py +99 -0
- velune/context/token_counter.py +134 -0
- velune/context/utilization.py +33 -0
- velune/context/window.py +63 -0
- velune/core/__init__.py +89 -0
- velune/core/background.py +5 -0
- velune/core/config/__init__.py +37 -0
- velune/core/errors/__init__.py +90 -0
- velune/core/errors/catalog.py +188 -0
- velune/core/errors/execution.py +31 -0
- velune/core/errors/memory.py +25 -0
- velune/core/errors/orchestration.py +31 -0
- velune/core/errors/provider.py +37 -0
- velune/core/event_loop.py +35 -0
- velune/core/logging.py +83 -0
- velune/core/paths.py +165 -0
- velune/core/runtime.py +113 -0
- velune/core/startup_profiler.py +56 -0
- velune/core/task_registry.py +117 -0
- velune/core/trace.py +83 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +53 -0
- velune/core/types/context.py +42 -0
- velune/core/types/inference.py +38 -0
- velune/core/types/memory.py +42 -0
- velune/core/types/model.py +70 -0
- velune/core/types/provider.py +62 -0
- velune/core/types/repository.py +38 -0
- velune/core/types/task.py +61 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +127 -0
- velune/daemon/transport.py +179 -0
- velune/events.py +204 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +315 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +130 -0
- velune/execution/command_spec.py +165 -0
- velune/execution/diff_preview.py +197 -0
- velune/execution/executor.py +181 -0
- velune/execution/module.py +18 -0
- velune/execution/multi_diff.py +67 -0
- velune/execution/path_guard.py +74 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +89 -0
- velune/execution/sandbox.py +268 -0
- velune/execution/validator.py +115 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +192 -0
- velune/kernel/__init__.py +55 -0
- velune/kernel/bootstrap.py +125 -0
- velune/kernel/config.py +426 -0
- velune/kernel/entrypoint.py +78 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +143 -0
- velune/kernel/module.py +17 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +96 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +115 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +624 -0
- velune/memory/__init__.py +32 -0
- velune/memory/compaction.py +506 -0
- velune/memory/embedding_pipeline.py +241 -0
- velune/memory/lifecycle.py +680 -0
- velune/memory/module.py +218 -0
- velune/memory/prioritizer.py +67 -0
- velune/memory/storage/episodic_schema.sql +53 -0
- velune/memory/storage/lancedb_store.py +282 -0
- velune/memory/storage/sqlite_manager.py +369 -0
- velune/memory/storage/sqlite_pool.py +149 -0
- velune/memory/tiers/episodic.py +588 -0
- velune/memory/tiers/graph.py +378 -0
- velune/memory/tiers/lineage.py +416 -0
- velune/memory/tiers/semantic.py +475 -0
- velune/memory/tiers/working.py +168 -0
- velune/memory/vitality.py +132 -0
- velune/models/__init__.py +15 -0
- velune/models/family.py +76 -0
- velune/models/module.py +20 -0
- velune/models/probes.py +192 -0
- velune/models/profile_cache.py +84 -0
- velune/models/profiler.py +108 -0
- velune/models/registry.py +251 -0
- velune/models/scorer.py +233 -0
- velune/models/specializations.py +205 -0
- velune/orchestration/__init__.py +19 -0
- velune/orchestration/engine.py +239 -0
- velune/orchestration/module.py +15 -0
- velune/orchestration/role_assignments.py +82 -0
- velune/orchestration/schemas.py +98 -0
- velune/plugins/__init__.py +20 -0
- velune/plugins/hooks.py +50 -0
- velune/plugins/loader.py +161 -0
- velune/plugins/registry.py +56 -0
- velune/plugins/schemas.py +21 -0
- velune/providers/__init__.py +23 -0
- velune/providers/adapters/anthropic.py +257 -0
- velune/providers/adapters/fireworks.py +115 -0
- velune/providers/adapters/google.py +234 -0
- velune/providers/adapters/groq.py +151 -0
- velune/providers/adapters/huggingface.py +210 -0
- velune/providers/adapters/llamacpp.py +208 -0
- velune/providers/adapters/lmstudio.py +175 -0
- velune/providers/adapters/ollama.py +233 -0
- velune/providers/adapters/openai.py +213 -0
- velune/providers/adapters/openrouter.py +81 -0
- velune/providers/adapters/together.py +134 -0
- velune/providers/adapters/xai.py +60 -0
- velune/providers/base.py +86 -0
- velune/providers/benchmarker.py +138 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +79 -0
- velune/providers/discovery/benchmarks.py +44 -0
- velune/providers/discovery/classifier.py +69 -0
- velune/providers/discovery/fireworks.py +95 -0
- velune/providers/discovery/gguf.py +88 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +117 -0
- velune/providers/discovery/groq.py +21 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +162 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +115 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/health.py +67 -0
- velune/providers/health_monitor.py +169 -0
- velune/providers/keystore.py +142 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +229 -0
- velune/providers/module.py +51 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +220 -0
- velune/providers/router.py +255 -0
- velune/providers/task_classifier.py +288 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +33 -0
- velune/repository/analyzer.py +127 -0
- velune/repository/ast_parser.py +822 -0
- velune/repository/blast_radius.py +298 -0
- velune/repository/boundary_classifier.py +295 -0
- velune/repository/cognition.py +316 -0
- velune/repository/grapher.py +179 -0
- velune/repository/import_graph.py +263 -0
- velune/repository/incremental_indexer.py +275 -0
- velune/repository/index_state.py +96 -0
- velune/repository/indexer.py +243 -0
- velune/repository/module.py +17 -0
- velune/repository/parser.py +474 -0
- velune/repository/project_type.py +300 -0
- velune/repository/rename_journal.py +287 -0
- velune/repository/scanner.py +193 -0
- velune/repository/schemas.py +102 -0
- velune/repository/symbol_registry.py +365 -0
- velune/repository/tracker.py +252 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/cache.py +110 -0
- velune/retrieval/fast_path.py +391 -0
- velune/retrieval/graph.py +124 -0
- velune/retrieval/hybrid.py +271 -0
- velune/retrieval/keyword.py +131 -0
- velune/retrieval/module.py +26 -0
- velune/retrieval/pipeline.py +303 -0
- velune/retrieval/reranker.py +102 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/slow_path.py +364 -0
- velune/retrieval/vector.py +203 -0
- velune/telemetry/__init__.py +59 -0
- velune/telemetry/cognition.py +267 -0
- velune/telemetry/cost_estimator.py +92 -0
- velune/telemetry/debug.py +304 -0
- velune/telemetry/doctor.py +244 -0
- velune/telemetry/logging.py +286 -0
- velune/telemetry/spans.py +277 -0
- velune/telemetry/token_tracker.py +140 -0
- velune/telemetry/usage_tracker.py +340 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +87 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +116 -0
- velune/tools/code/search.py +123 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +136 -0
- velune/tools/filesystem/write.py +163 -0
- velune/tools/git/history.py +177 -0
- velune/tools/git/operations.py +122 -0
- velune/tools/git/state.py +121 -0
- velune/tools/module.py +81 -0
- velune/tools/terminal/execute.py +72 -0
- velune/tools/terminal/history.py +47 -0
- velune/tools/web/fetch.py +55 -0
- velune/tools/web/validator.py +122 -0
- velune_cli-0.9.0.dist-info/METADATA +518 -0
- velune_cli-0.9.0.dist-info/RECORD +279 -0
- velune_cli-0.9.0.dist-info/WHEEL +4 -0
- velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
- velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
"""Memory Subsystem Lifecycle and Coordinator.
|
|
2
|
+
|
|
3
|
+
Manages clean connections startup, shutdown flushes, and orchestrates
|
|
4
|
+
transient to persistent memory ingestion.
|
|
5
|
+
|
|
6
|
+
Phase 1 repairs:
|
|
7
|
+
* ``shutdown()`` now flushes any live working-memory turns into the
|
|
8
|
+
episodic SQLite tier before terminating. This ensures turn history
|
|
9
|
+
survives process exit and is not silently discarded.
|
|
10
|
+
* ``get_recent_context(session_id, limit)`` exposes the minimal episodic
|
|
11
|
+
read path so orchestrators can hydrate context from previous sessions
|
|
12
|
+
without importing the episodic tier directly.
|
|
13
|
+
|
|
14
|
+
Phase 2a: Enhanced MemoryLifecycleManager with:
|
|
15
|
+
* Multi-tier retrieval: working → episodic (LIKE) → semantic (ANN) → merged
|
|
16
|
+
* Vitality-based filtering: LIVE/ZOMBIE/ARCHIVED classification
|
|
17
|
+
* Lineage warnings: architectural decisions and failed experiments
|
|
18
|
+
* Health reporting: turn counts, queue depth, store size
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import logging
|
|
25
|
+
import time
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from pydantic import BaseModel, Field
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger("velune.memory.lifecycle")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MemoryArtifact:
|
|
35
|
+
"""Represents a discrete memory chunk captured during run finalization."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
id: str,
|
|
40
|
+
memory_type: str,
|
|
41
|
+
content: str,
|
|
42
|
+
importance: float,
|
|
43
|
+
metadata: dict[str, Any],
|
|
44
|
+
) -> None:
|
|
45
|
+
self.id = id
|
|
46
|
+
self.memory_type = memory_type
|
|
47
|
+
self.content = content
|
|
48
|
+
self.importance = importance
|
|
49
|
+
self.metadata = metadata
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
# Phase 2a: Multi-tier retrieval and vitality-based filtering
|
|
54
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RetrievedResult(BaseModel):
|
|
58
|
+
"""A single result from multi-tier memory retrieval."""
|
|
59
|
+
|
|
60
|
+
content: str
|
|
61
|
+
source_type: str # "working" | "episodic" | "semantic"
|
|
62
|
+
relevance_score: float = 0.0 # 0-1, higher is more relevant
|
|
63
|
+
trust_score: float = 1.0 # 0-1, influenced by vitality
|
|
64
|
+
vitality: str = "live" # "live" | "zombie" | "archived"
|
|
65
|
+
session_id: str = ""
|
|
66
|
+
age_seconds: float = 0.0
|
|
67
|
+
attribution: str = "" # e.g., "3 days ago" or "current session"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class RetrievedContext(BaseModel):
|
|
71
|
+
"""Aggregated context returned from multi-tier retrieval."""
|
|
72
|
+
|
|
73
|
+
results: list[RetrievedResult] = Field(default_factory=list)
|
|
74
|
+
total_tokens: int = 0
|
|
75
|
+
query: str = ""
|
|
76
|
+
fallback_to_zombie: bool = False # Whether ZOMBIE tier was accessed
|
|
77
|
+
workspace_root: str = ""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Decision(BaseModel):
|
|
81
|
+
"""An architectural decision from the lineage tier."""
|
|
82
|
+
|
|
83
|
+
id: str
|
|
84
|
+
target_subsystem: str
|
|
85
|
+
rationale: str
|
|
86
|
+
architectural_impact: float = 0.0
|
|
87
|
+
consequences: str | None = None
|
|
88
|
+
timestamp: float = 0.0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Failure(BaseModel):
|
|
92
|
+
"""A failed experiment from the lineage tier."""
|
|
93
|
+
|
|
94
|
+
id: int
|
|
95
|
+
target_subsystem: str
|
|
96
|
+
error_type: str
|
|
97
|
+
error_message: str
|
|
98
|
+
timestamp: float = 0.0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class MemoryHealth:
|
|
103
|
+
"""Health metrics for the memory subsystem."""
|
|
104
|
+
|
|
105
|
+
working_memory_turns: int = 0
|
|
106
|
+
episodic_sessions: int = 0
|
|
107
|
+
semantic_indexed_count: int = 0
|
|
108
|
+
embedding_queue_depth: int = 0
|
|
109
|
+
lancedb_size_mb: float = 0.0
|
|
110
|
+
timestamp: float = 0.0
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> dict[str, Any]:
|
|
113
|
+
"""Convert to dictionary for display."""
|
|
114
|
+
return {
|
|
115
|
+
"working_memory_turns": self.working_memory_turns,
|
|
116
|
+
"episodic_sessions": self.episodic_sessions,
|
|
117
|
+
"semantic_indexed_count": self.semantic_indexed_count,
|
|
118
|
+
"embedding_queue_depth": self.embedding_queue_depth,
|
|
119
|
+
"lancedb_size_mb": self.lancedb_size_mb,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class MemoryLifecycleCoordinator:
|
|
124
|
+
"""Orchestrates memory subsystem boot protocols and handles artifact ingestion."""
|
|
125
|
+
|
|
126
|
+
def __init__(self, working_tier: Any, episodic_tier: Any) -> None:
|
|
127
|
+
self.working = working_tier
|
|
128
|
+
self.episodic = episodic_tier
|
|
129
|
+
self._is_active = False
|
|
130
|
+
|
|
131
|
+
async def startup(self) -> None:
|
|
132
|
+
"""Boot databases and establish active connection boundaries."""
|
|
133
|
+
logger.info("Initializing Hierarchical Memory Tiers...")
|
|
134
|
+
self._is_active = True
|
|
135
|
+
|
|
136
|
+
async def shutdown(self) -> None:
|
|
137
|
+
"""Flush working memory turns into episodic SQLite, then terminate.
|
|
138
|
+
|
|
139
|
+
Previously this was a no-op (only logged a message). Now it
|
|
140
|
+
iterates all live working turns and persists them to the episodic
|
|
141
|
+
tier before clearing them, so turn history survives process exit.
|
|
142
|
+
"""
|
|
143
|
+
logger.info("Flushing transient working memory buffers before shutdown...")
|
|
144
|
+
|
|
145
|
+
if self.working and self.episodic:
|
|
146
|
+
session_id = getattr(self.working, "session_id", "default")
|
|
147
|
+
# Evict stale turns first so we don't persist garbage
|
|
148
|
+
if hasattr(self.working, "evict_expired"):
|
|
149
|
+
self.working.evict_expired()
|
|
150
|
+
|
|
151
|
+
turns = self.working.get_turns()
|
|
152
|
+
if turns:
|
|
153
|
+
logger.info(
|
|
154
|
+
"Persisting %d working memory turns to episodic SQLite [session=%s]",
|
|
155
|
+
len(turns),
|
|
156
|
+
session_id,
|
|
157
|
+
)
|
|
158
|
+
for turn in turns:
|
|
159
|
+
try:
|
|
160
|
+
await self.episodic.add_turn(
|
|
161
|
+
session_id=session_id,
|
|
162
|
+
role=turn.role,
|
|
163
|
+
content=turn.content,
|
|
164
|
+
metadata=turn.metadata,
|
|
165
|
+
)
|
|
166
|
+
except Exception as exc:
|
|
167
|
+
logger.warning("Failed to flush turn to episodic memory: %s", exc)
|
|
168
|
+
|
|
169
|
+
self._is_active = False
|
|
170
|
+
|
|
171
|
+
async def get_recent_context(self, session_id: str, limit: int = 10) -> list[dict[str, Any]]:
|
|
172
|
+
"""Return the most recent episodic turns for a session.
|
|
173
|
+
|
|
174
|
+
This is the *minimal episodic read path* — it reads directly from
|
|
175
|
+
the episodic SQLite tier so orchestrators can hydrate context from
|
|
176
|
+
previous sessions without coupling to the full retrieval stack.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
session_id:
|
|
181
|
+
The session whose history to fetch.
|
|
182
|
+
limit:
|
|
183
|
+
Maximum number of turns to return (most recent first).
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
list[dict]:
|
|
188
|
+
Each dict has keys ``role``, ``content``, ``timestamp``,
|
|
189
|
+
``metadata``. Returns an empty list if the episodic tier is
|
|
190
|
+
unavailable or has no history for this session.
|
|
191
|
+
"""
|
|
192
|
+
if not self.episodic:
|
|
193
|
+
return []
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
turns = await self.episodic.get_turns(session_id)
|
|
197
|
+
recent = turns[-limit:]
|
|
198
|
+
return [
|
|
199
|
+
{
|
|
200
|
+
"role": t.role,
|
|
201
|
+
"content": t.content,
|
|
202
|
+
"timestamp": t.timestamp,
|
|
203
|
+
"metadata": t.metadata,
|
|
204
|
+
}
|
|
205
|
+
for t in recent
|
|
206
|
+
]
|
|
207
|
+
except Exception as exc:
|
|
208
|
+
logger.warning("Failed to read episodic context [session=%s]: %s", session_id, exc)
|
|
209
|
+
return []
|
|
210
|
+
|
|
211
|
+
def ingest(self, artifact: MemoryArtifact) -> None:
|
|
212
|
+
"""Ingest a finalized memory artifact into working and episodic tiers."""
|
|
213
|
+
session_id = artifact.metadata.get("run_id") or "default"
|
|
214
|
+
if self.working:
|
|
215
|
+
self.working.add_turn(
|
|
216
|
+
role="system",
|
|
217
|
+
content=artifact.content,
|
|
218
|
+
metadata=artifact.metadata,
|
|
219
|
+
)
|
|
220
|
+
if self.episodic:
|
|
221
|
+
self.episodic.add_turn(
|
|
222
|
+
session_id=session_id,
|
|
223
|
+
role="system",
|
|
224
|
+
content=artifact.content,
|
|
225
|
+
metadata=artifact.metadata,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def summary(self) -> dict[str, Any]:
|
|
229
|
+
"""Retrieve dynamic health and retention stats across all tiers."""
|
|
230
|
+
return {
|
|
231
|
+
"working_turns": len(self.working.get_turns()) if self.working else 0,
|
|
232
|
+
"working_logs": len(self.working.get_execution_logs()) if self.working else 0,
|
|
233
|
+
"is_active": self._is_active,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
238
|
+
# Phase 2a: Enhanced MemoryLifecycleManager
|
|
239
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class MemoryLifecycleManager:
|
|
243
|
+
"""Coordinates all memory tiers: working, episodic, semantic, and lineage.
|
|
244
|
+
|
|
245
|
+
Provides unified read (retrieve) and write (record_turn) interfaces,
|
|
246
|
+
vitality-based filtering, and health metrics.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
def __init__(
|
|
250
|
+
self,
|
|
251
|
+
working_tier: Any,
|
|
252
|
+
episodic_memory: Any,
|
|
253
|
+
semantic_memory: Any,
|
|
254
|
+
embedding_pipeline: Any,
|
|
255
|
+
lineage_tier: Any,
|
|
256
|
+
episodic_session_memory: Any | None = None,
|
|
257
|
+
) -> None:
|
|
258
|
+
"""Initialize with all memory tier implementations.
|
|
259
|
+
|
|
260
|
+
Parameters
|
|
261
|
+
----------
|
|
262
|
+
working_tier:
|
|
263
|
+
In-process turn store (WorkingMemoryTier).
|
|
264
|
+
episodic_memory:
|
|
265
|
+
SQLite-backed session/turn store (EpisodicMemory).
|
|
266
|
+
semantic_memory:
|
|
267
|
+
LanceDB-backed semantic store (SemanticMemory).
|
|
268
|
+
embedding_pipeline:
|
|
269
|
+
Background embedding queue (EmbeddingPipeline).
|
|
270
|
+
lineage_tier:
|
|
271
|
+
Decision and failure store (LineageMemoryTier).
|
|
272
|
+
episodic_session_memory:
|
|
273
|
+
Optional session memory tier (EpisodicMemoryTier, legacy).
|
|
274
|
+
"""
|
|
275
|
+
self.working = working_tier
|
|
276
|
+
self.episodic_memory = episodic_memory
|
|
277
|
+
self.semantic_memory = semantic_memory
|
|
278
|
+
self.embedding_pipeline = embedding_pipeline
|
|
279
|
+
self.lineage = lineage_tier
|
|
280
|
+
self.episodic_session_memory = episodic_session_memory
|
|
281
|
+
|
|
282
|
+
from velune.memory.vitality import VitalityClassifier
|
|
283
|
+
|
|
284
|
+
self._vitality = VitalityClassifier()
|
|
285
|
+
self._session_count = 0
|
|
286
|
+
self._is_active = False
|
|
287
|
+
|
|
288
|
+
async def startup(self) -> None:
|
|
289
|
+
"""Initialize all memory tiers."""
|
|
290
|
+
logger.info("MemoryLifecycleManager: starting up...")
|
|
291
|
+
self._is_active = True
|
|
292
|
+
|
|
293
|
+
async def shutdown(self) -> None:
|
|
294
|
+
"""Flush queues and close connections."""
|
|
295
|
+
logger.info("MemoryLifecycleManager: shutting down...")
|
|
296
|
+
if self.working and self.episodic_memory:
|
|
297
|
+
session_id = getattr(self.working, "session_id", "default")
|
|
298
|
+
if hasattr(self.working, "evict_expired"):
|
|
299
|
+
self.working.evict_expired()
|
|
300
|
+
turns = self.working.get_turns()
|
|
301
|
+
if turns:
|
|
302
|
+
logger.debug("Flushing %d working turns to episodic", len(turns))
|
|
303
|
+
for turn in turns:
|
|
304
|
+
try:
|
|
305
|
+
await self.episodic_memory.record_turn(
|
|
306
|
+
session_id=session_id,
|
|
307
|
+
role=turn.role,
|
|
308
|
+
content=turn.content,
|
|
309
|
+
)
|
|
310
|
+
except Exception as exc:
|
|
311
|
+
logger.warning("Failed to flush turn: %s", exc)
|
|
312
|
+
self._is_active = False
|
|
313
|
+
|
|
314
|
+
async def record_turn(
|
|
315
|
+
self,
|
|
316
|
+
session_id: str,
|
|
317
|
+
role: str,
|
|
318
|
+
content: str,
|
|
319
|
+
model: str | None = None,
|
|
320
|
+
tokens: int | None = None,
|
|
321
|
+
workspace_root: str = "",
|
|
322
|
+
) -> str:
|
|
323
|
+
"""Record a conversation turn across all applicable tiers.
|
|
324
|
+
|
|
325
|
+
Writes to episodic SQLite, enqueues for semantic embedding,
|
|
326
|
+
and logs to working memory. Triggers compaction if thresholds are met.
|
|
327
|
+
|
|
328
|
+
Returns the turn ID.
|
|
329
|
+
"""
|
|
330
|
+
turn_id = ""
|
|
331
|
+
try:
|
|
332
|
+
if self.episodic_memory:
|
|
333
|
+
turn_id = await self.episodic_memory.record_turn(
|
|
334
|
+
session_id=session_id,
|
|
335
|
+
role=role,
|
|
336
|
+
content=content,
|
|
337
|
+
model=model,
|
|
338
|
+
tokens=tokens,
|
|
339
|
+
)
|
|
340
|
+
except Exception as exc:
|
|
341
|
+
logger.warning("Failed to record turn to episodic: %s", exc)
|
|
342
|
+
|
|
343
|
+
if self.semantic_memory and turn_id:
|
|
344
|
+
try:
|
|
345
|
+
from velune.memory.tiers.episodic import Turn
|
|
346
|
+
|
|
347
|
+
turn = Turn(
|
|
348
|
+
id=turn_id,
|
|
349
|
+
session_id=session_id,
|
|
350
|
+
turn_index=0,
|
|
351
|
+
role=role,
|
|
352
|
+
content=content,
|
|
353
|
+
model_used=model,
|
|
354
|
+
tokens_used=tokens,
|
|
355
|
+
created_at=time.time(),
|
|
356
|
+
)
|
|
357
|
+
self.semantic_memory.index_turn(turn, workspace_root)
|
|
358
|
+
except Exception as exc:
|
|
359
|
+
logger.debug("Failed to enqueue turn for embedding: %s", exc)
|
|
360
|
+
|
|
361
|
+
if self.working:
|
|
362
|
+
try:
|
|
363
|
+
self.working.add_turn(role, content, {"model": model, "tokens": tokens})
|
|
364
|
+
except Exception as exc:
|
|
365
|
+
logger.warning("Failed to add turn to working: %s", exc)
|
|
366
|
+
|
|
367
|
+
# Check for compaction trigger (non-blocking)
|
|
368
|
+
await self._check_and_trigger_compaction(session_id)
|
|
369
|
+
|
|
370
|
+
return turn_id
|
|
371
|
+
|
|
372
|
+
async def _check_and_trigger_compaction(self, session_id: str) -> None:
|
|
373
|
+
"""Check if compaction should be triggered and schedule it as background task.
|
|
374
|
+
|
|
375
|
+
Parameters
|
|
376
|
+
----------
|
|
377
|
+
session_id:
|
|
378
|
+
Session ID for the current session
|
|
379
|
+
"""
|
|
380
|
+
if not self.working:
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
turns = self.working.get_turns()
|
|
385
|
+
turn_count = len(turns)
|
|
386
|
+
|
|
387
|
+
# Estimate current token count (rough)
|
|
388
|
+
current_tokens = sum(len(t.content) // 4 for t in turns)
|
|
389
|
+
|
|
390
|
+
# Create compactor if needed
|
|
391
|
+
if not hasattr(self, "_compactor"):
|
|
392
|
+
from velune.memory.compaction import ContextCompactor
|
|
393
|
+
|
|
394
|
+
# Use first available provider (or default)
|
|
395
|
+
provider = None # Will be set when needed
|
|
396
|
+
self._compactor = ContextCompactor(
|
|
397
|
+
provider=provider,
|
|
398
|
+
working_tier=self.working,
|
|
399
|
+
episodic_memory=self.episodic_memory,
|
|
400
|
+
max_context_tokens=100000,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Check if compaction should trigger
|
|
404
|
+
should_compact = await self._compactor.should_compact(
|
|
405
|
+
turn_count=turn_count,
|
|
406
|
+
current_token_count=current_tokens,
|
|
407
|
+
session_end=False,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
if should_compact:
|
|
411
|
+
# Schedule compaction as background task (non-blocking)
|
|
412
|
+
asyncio.create_task(self._perform_compaction(session_id))
|
|
413
|
+
except Exception as exc:
|
|
414
|
+
logger.debug("Error checking compaction trigger: %s", exc)
|
|
415
|
+
|
|
416
|
+
async def _perform_compaction(self, session_id: str) -> None:
|
|
417
|
+
"""Perform compaction asynchronously in the background.
|
|
418
|
+
|
|
419
|
+
Parameters
|
|
420
|
+
----------
|
|
421
|
+
session_id:
|
|
422
|
+
Session ID for the current session
|
|
423
|
+
"""
|
|
424
|
+
try:
|
|
425
|
+
if hasattr(self, "_compactor"):
|
|
426
|
+
stats = await self._compactor.compact(session_id)
|
|
427
|
+
if stats:
|
|
428
|
+
logger.info(
|
|
429
|
+
f"Compaction completed: {stats.turns_compacted} turns → "
|
|
430
|
+
f"{stats.summary_token_count} tokens ({stats.compression_ratio:.1f}x)"
|
|
431
|
+
)
|
|
432
|
+
except Exception as exc:
|
|
433
|
+
logger.warning("Background compaction failed: %s", exc)
|
|
434
|
+
|
|
435
|
+
async def retrieve(
|
|
436
|
+
self,
|
|
437
|
+
query: str,
|
|
438
|
+
workspace_root: str,
|
|
439
|
+
budget: int = 4000,
|
|
440
|
+
) -> RetrievedContext:
|
|
441
|
+
"""Multi-tier retrieval: working → episodic (LIKE) → semantic (ANN).
|
|
442
|
+
|
|
443
|
+
Merges results from all tiers, filters by vitality, ranks by
|
|
444
|
+
(relevance × trust), and fits to token budget.
|
|
445
|
+
|
|
446
|
+
Parameters
|
|
447
|
+
----------
|
|
448
|
+
query:
|
|
449
|
+
The search query.
|
|
450
|
+
workspace_root:
|
|
451
|
+
Workspace path for scoping searches.
|
|
452
|
+
budget:
|
|
453
|
+
Token budget for results (approx 4000 tokens default).
|
|
454
|
+
|
|
455
|
+
Returns
|
|
456
|
+
-------
|
|
457
|
+
RetrievedContext:
|
|
458
|
+
Aggregated results, sorted by relevance.
|
|
459
|
+
"""
|
|
460
|
+
context = RetrievedContext(query=query, workspace_root=workspace_root)
|
|
461
|
+
accumulated_tokens = 0
|
|
462
|
+
|
|
463
|
+
try:
|
|
464
|
+
# Step 1: Working memory (current session, fastest)
|
|
465
|
+
if self.working:
|
|
466
|
+
recent = self.working.get_recent_turns(limit=10)
|
|
467
|
+
for turn in recent:
|
|
468
|
+
result = RetrievedResult(
|
|
469
|
+
content=turn.content,
|
|
470
|
+
source_type="working",
|
|
471
|
+
relevance_score=0.95, # High confidence for current session
|
|
472
|
+
trust_score=1.0,
|
|
473
|
+
vitality="live",
|
|
474
|
+
session_id=turn.session_id,
|
|
475
|
+
age_seconds=time.time() - turn.timestamp,
|
|
476
|
+
attribution="current session",
|
|
477
|
+
)
|
|
478
|
+
tokens = self._estimate_tokens(turn.content)
|
|
479
|
+
if accumulated_tokens + tokens <= budget:
|
|
480
|
+
context.results.append(result)
|
|
481
|
+
accumulated_tokens += tokens
|
|
482
|
+
else:
|
|
483
|
+
break
|
|
484
|
+
except Exception as exc:
|
|
485
|
+
logger.debug("Working memory search failed: %s", exc)
|
|
486
|
+
|
|
487
|
+
# Step 2: Episodic memory (SQLite LIKE, fast but imprecise)
|
|
488
|
+
try:
|
|
489
|
+
if self.episodic_memory and accumulated_tokens < budget:
|
|
490
|
+
episodic_results = await self.episodic_memory.search_by_content(
|
|
491
|
+
query, workspace_root, limit=10
|
|
492
|
+
)
|
|
493
|
+
for turn in episodic_results:
|
|
494
|
+
age = time.time() - turn.created_at
|
|
495
|
+
result = RetrievedResult(
|
|
496
|
+
content=turn.content,
|
|
497
|
+
source_type="episodic",
|
|
498
|
+
relevance_score=0.7,
|
|
499
|
+
trust_score=0.8,
|
|
500
|
+
vitality="zombie" if age > 604800 else "live", # 7 days
|
|
501
|
+
session_id=turn.session_id,
|
|
502
|
+
age_seconds=age,
|
|
503
|
+
attribution=self._format_age(age),
|
|
504
|
+
)
|
|
505
|
+
tokens = self._estimate_tokens(turn.content)
|
|
506
|
+
if accumulated_tokens + tokens <= budget:
|
|
507
|
+
context.results.append(result)
|
|
508
|
+
accumulated_tokens += tokens
|
|
509
|
+
else:
|
|
510
|
+
break
|
|
511
|
+
except Exception as exc:
|
|
512
|
+
logger.debug("Episodic search failed: %s", exc)
|
|
513
|
+
|
|
514
|
+
# Step 3: Semantic memory (LanceDB ANN, slower but precise)
|
|
515
|
+
try:
|
|
516
|
+
if self.semantic_memory and accumulated_tokens < budget and self.embedding_pipeline:
|
|
517
|
+
semantic_results = await self.semantic_memory.search(query, workspace_root, limit=5)
|
|
518
|
+
for mem in semantic_results:
|
|
519
|
+
# Map semantic memory's vitality to our enum
|
|
520
|
+
vitality = "live"
|
|
521
|
+
trust = mem.trust_score
|
|
522
|
+
if mem.age_seconds > 2592000: # 30 days
|
|
523
|
+
vitality = "archived"
|
|
524
|
+
trust *= 0.2
|
|
525
|
+
elif mem.age_seconds > 604800: # 7 days
|
|
526
|
+
vitality = "zombie"
|
|
527
|
+
trust *= 0.6
|
|
528
|
+
|
|
529
|
+
result = RetrievedResult(
|
|
530
|
+
content=mem.content,
|
|
531
|
+
source_type="semantic",
|
|
532
|
+
relevance_score=1.0 - mem.distance,
|
|
533
|
+
trust_score=trust,
|
|
534
|
+
vitality=vitality,
|
|
535
|
+
session_id=mem.session_id,
|
|
536
|
+
age_seconds=mem.age_seconds,
|
|
537
|
+
attribution=mem.attribution,
|
|
538
|
+
)
|
|
539
|
+
tokens = self._estimate_tokens(mem.content)
|
|
540
|
+
if accumulated_tokens + tokens <= budget:
|
|
541
|
+
context.results.append(result)
|
|
542
|
+
accumulated_tokens += tokens
|
|
543
|
+
else:
|
|
544
|
+
break
|
|
545
|
+
except Exception as exc:
|
|
546
|
+
logger.debug("Semantic search failed: %s", exc)
|
|
547
|
+
|
|
548
|
+
# Step 4: Rank by (relevance × trust) and sort
|
|
549
|
+
for result in context.results:
|
|
550
|
+
result.relevance_score *= result.trust_score
|
|
551
|
+
|
|
552
|
+
context.results.sort(key=lambda r: r.relevance_score, reverse=True)
|
|
553
|
+
context.total_tokens = accumulated_tokens
|
|
554
|
+
|
|
555
|
+
logger.debug(
|
|
556
|
+
"Retrieved %d results (%d tokens) for query '%s'",
|
|
557
|
+
len(context.results),
|
|
558
|
+
accumulated_tokens,
|
|
559
|
+
query[:50],
|
|
560
|
+
)
|
|
561
|
+
return context
|
|
562
|
+
|
|
563
|
+
async def get_working_context(self, session_id: str, limit: int = 10) -> list[Any]:
|
|
564
|
+
"""Return the N most recent turns from working memory.
|
|
565
|
+
|
|
566
|
+
Parameters
|
|
567
|
+
----------
|
|
568
|
+
session_id:
|
|
569
|
+
The session whose context to return.
|
|
570
|
+
limit:
|
|
571
|
+
Maximum number of turns to return.
|
|
572
|
+
|
|
573
|
+
Returns
|
|
574
|
+
-------
|
|
575
|
+
list:
|
|
576
|
+
Recent turns in chronological order.
|
|
577
|
+
"""
|
|
578
|
+
if not self.working:
|
|
579
|
+
return []
|
|
580
|
+
try:
|
|
581
|
+
return self.working.get_recent_turns(limit)
|
|
582
|
+
except Exception as exc:
|
|
583
|
+
logger.warning("Failed to get working context: %s", exc)
|
|
584
|
+
return []
|
|
585
|
+
|
|
586
|
+
async def get_lineage_warnings(self, query: str) -> tuple[list[Decision], list[Failure]]:
|
|
587
|
+
"""Retrieve architectural decisions and failed experiments related to query.
|
|
588
|
+
|
|
589
|
+
Returns two lists: approved decisions and past failures that might inform
|
|
590
|
+
the current decision.
|
|
591
|
+
|
|
592
|
+
Parameters
|
|
593
|
+
----------
|
|
594
|
+
query:
|
|
595
|
+
The query context (e.g., a subsystem name or feature description).
|
|
596
|
+
|
|
597
|
+
Returns
|
|
598
|
+
-------
|
|
599
|
+
tuple[list[Decision], list[Failure]]:
|
|
600
|
+
(decisions, failures) both sorted by relevance.
|
|
601
|
+
"""
|
|
602
|
+
decisions: list[Decision] = []
|
|
603
|
+
failures: list[Failure] = []
|
|
604
|
+
|
|
605
|
+
if not self.lineage:
|
|
606
|
+
return decisions, failures
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
# This is a stub; full implementation requires searching lineage by query
|
|
610
|
+
# For now, return empty lists and log the intention.
|
|
611
|
+
logger.debug("Lineage warnings requested for query: %s", query)
|
|
612
|
+
except Exception as exc:
|
|
613
|
+
logger.warning("Failed to retrieve lineage warnings: %s", exc)
|
|
614
|
+
|
|
615
|
+
return decisions, failures
|
|
616
|
+
|
|
617
|
+
async def health(self) -> MemoryHealth:
|
|
618
|
+
"""Return health metrics across all memory tiers.
|
|
619
|
+
|
|
620
|
+
Returns
|
|
621
|
+
-------
|
|
622
|
+
MemoryHealth:
|
|
623
|
+
Metrics including turn counts, queue depth, and store size.
|
|
624
|
+
"""
|
|
625
|
+
health = MemoryHealth(timestamp=time.time())
|
|
626
|
+
|
|
627
|
+
if self.working:
|
|
628
|
+
health.working_memory_turns = len(self.working.get_turns())
|
|
629
|
+
|
|
630
|
+
if self.episodic_memory:
|
|
631
|
+
try:
|
|
632
|
+
sessions = await self.episodic_memory.list_recent_sessions(
|
|
633
|
+
workspace_root="", limit=1000
|
|
634
|
+
)
|
|
635
|
+
health.episodic_sessions = len(sessions)
|
|
636
|
+
except Exception as exc:
|
|
637
|
+
logger.debug("Failed to count episodic sessions: %s", exc)
|
|
638
|
+
|
|
639
|
+
if self.embedding_pipeline:
|
|
640
|
+
health.embedding_queue_depth = self.embedding_pipeline._queue.qsize()
|
|
641
|
+
|
|
642
|
+
if hasattr(self.semantic_memory, "_store") and self.semantic_memory._store:
|
|
643
|
+
try:
|
|
644
|
+
import os
|
|
645
|
+
|
|
646
|
+
store_path = getattr(self.semantic_memory._store, "_path", None)
|
|
647
|
+
if store_path and os.path.isdir(store_path):
|
|
648
|
+
total_size = sum(
|
|
649
|
+
os.path.getsize(os.path.join(root, f))
|
|
650
|
+
for root, _, files in os.walk(store_path)
|
|
651
|
+
for f in files
|
|
652
|
+
)
|
|
653
|
+
health.lancedb_size_mb = total_size / (1024 * 1024)
|
|
654
|
+
except Exception as exc:
|
|
655
|
+
logger.debug("Failed to measure LanceDB size: %s", exc)
|
|
656
|
+
|
|
657
|
+
return health
|
|
658
|
+
|
|
659
|
+
@staticmethod
|
|
660
|
+
def _estimate_tokens(text: str) -> int:
|
|
661
|
+
"""Rough token estimate (4 chars ≈ 1 token)."""
|
|
662
|
+
return max(1, len(text) // 4)
|
|
663
|
+
|
|
664
|
+
@staticmethod
|
|
665
|
+
def _format_age(seconds: float) -> str:
|
|
666
|
+
"""Return a human-readable relative age string."""
|
|
667
|
+
minutes = seconds / 60
|
|
668
|
+
hours = minutes / 60
|
|
669
|
+
days = hours / 24
|
|
670
|
+
if days >= 2:
|
|
671
|
+
return f"{int(days)} days ago"
|
|
672
|
+
if days >= 1:
|
|
673
|
+
return "yesterday"
|
|
674
|
+
if hours >= 2:
|
|
675
|
+
return f"{int(hours)} hours ago"
|
|
676
|
+
if hours >= 1:
|
|
677
|
+
return "an hour ago"
|
|
678
|
+
if minutes >= 2:
|
|
679
|
+
return f"{int(minutes)} minutes ago"
|
|
680
|
+
return "just now"
|