superlocalmemory 3.4.9 → 3.4.11
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.
- package/README.md +23 -3
- package/docs/cloud-backup.md +174 -0
- package/docs/skill-evolution.md +256 -0
- package/ide/hooks/tool-event-hook.sh +101 -11
- package/package.json +1 -1
- package/pyproject.toml +3 -2
- package/src/superlocalmemory/cli/commands.py +359 -0
- package/src/superlocalmemory/cli/ingest_cmd.py +81 -29
- package/src/superlocalmemory/cli/main.py +32 -0
- package/src/superlocalmemory/cli/setup_wizard.py +54 -11
- package/src/superlocalmemory/core/config.py +35 -0
- package/src/superlocalmemory/core/consolidation_engine.py +138 -0
- package/src/superlocalmemory/core/embedding_worker.py +1 -1
- package/src/superlocalmemory/core/engine.py +19 -0
- package/src/superlocalmemory/core/fact_consolidator.py +425 -0
- package/src/superlocalmemory/core/graph_pruner.py +290 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +44 -3
- package/src/superlocalmemory/core/recall_pipeline.py +9 -0
- package/src/superlocalmemory/core/tier_manager.py +325 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +96 -28
- package/src/superlocalmemory/evolution/__init__.py +29 -0
- package/src/superlocalmemory/evolution/blind_verifier.py +115 -0
- package/src/superlocalmemory/evolution/evolution_store.py +302 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +181 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +555 -0
- package/src/superlocalmemory/evolution/triggers.py +367 -0
- package/src/superlocalmemory/evolution/types.py +92 -0
- package/src/superlocalmemory/hooks/hook_handlers.py +13 -0
- package/src/superlocalmemory/infra/backup.py +63 -20
- package/src/superlocalmemory/infra/cloud_backup.py +703 -0
- package/src/superlocalmemory/learning/skill_performance_miner.py +422 -0
- package/src/superlocalmemory/mcp/server.py +4 -0
- package/src/superlocalmemory/mcp/tools_evolution.py +338 -0
- package/src/superlocalmemory/retrieval/engine.py +64 -4
- package/src/superlocalmemory/retrieval/forgetting_filter.py +22 -7
- package/src/superlocalmemory/retrieval/strategy.py +2 -2
- package/src/superlocalmemory/server/routes/backup.py +512 -8
- package/src/superlocalmemory/server/routes/behavioral.py +39 -17
- package/src/superlocalmemory/server/routes/evolution.py +213 -0
- package/src/superlocalmemory/server/routes/tiers.py +195 -0
- package/src/superlocalmemory/server/unified_daemon.py +36 -5
- package/src/superlocalmemory/storage/schema_v3410.py +159 -0
- package/src/superlocalmemory/storage/schema_v3411.py +149 -0
- package/src/superlocalmemory/ui/index.html +59 -3
- package/src/superlocalmemory/ui/js/core.js +3 -0
- package/src/superlocalmemory/ui/js/lifecycle.js +83 -0
- package/src/superlocalmemory/ui/js/ng-entities.js +27 -3
- package/src/superlocalmemory/ui/js/ng-shell.js +33 -0
- package/src/superlocalmemory/ui/js/ng-skills.js +611 -0
- package/src/superlocalmemory/ui/js/settings.js +311 -1
- package/src/superlocalmemory.egg-info/PKG-INFO +16 -1
- package/src/superlocalmemory.egg-info/SOURCES.txt +18 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Skill Performance Miner — tracks per-skill effectiveness from tool events.
|
|
6
|
+
|
|
7
|
+
Zero-LLM approach: mines tool_events table for Skill tool invocations,
|
|
8
|
+
builds execution traces from surrounding events, computes approximate
|
|
9
|
+
outcome heuristics, and creates skill-level behavioral assertions.
|
|
10
|
+
|
|
11
|
+
Runs as Step 10 in the consolidation pipeline (after Step 9: soft prompts).
|
|
12
|
+
Depends on enriched tool_events (v3.4.10 hook with input_summary/output_summary).
|
|
13
|
+
|
|
14
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import sqlite3
|
|
23
|
+
from collections import Counter, defaultdict
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
# Thresholds — conservative to avoid hallucinating patterns
|
|
30
|
+
MIN_INVOCATIONS = 5 # Don't create assertions for skills with fewer uses
|
|
31
|
+
MIN_CONFIDENCE = 0.5 # Don't inject into soft prompts below this
|
|
32
|
+
TRACE_WINDOW = 10 # Number of tool events to look at after a Skill call
|
|
33
|
+
RETRY_WINDOW_SECONDS = 300 # 5 minutes — same Skill re-invoked = potential retry
|
|
34
|
+
REINFORCEMENT_NUDGE = 0.10 # Bayesian confidence increase per consolidation cycle
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SkillPerformanceMiner:
|
|
38
|
+
"""Mine tool_events for per-skill performance metrics.
|
|
39
|
+
|
|
40
|
+
Discovers patterns like:
|
|
41
|
+
- "brainstorming skill: 82% effective, 47 invocations, best for feature planning"
|
|
42
|
+
- "TDD + code-review used together: +23% effective vs individually"
|
|
43
|
+
- "brainstorm skill degraded: effective_rate dropped from 0.82 to 0.35"
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, db_path: str | Path):
|
|
47
|
+
self._db_path = str(db_path)
|
|
48
|
+
|
|
49
|
+
def mine(self, profile_id: str = "default") -> dict:
|
|
50
|
+
"""Run skill performance mining. Returns summary."""
|
|
51
|
+
result = {
|
|
52
|
+
"skills_found": 0,
|
|
53
|
+
"assertions_created": 0,
|
|
54
|
+
"assertions_reinforced": 0,
|
|
55
|
+
"entities_updated": 0,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
59
|
+
conn.row_factory = sqlite3.Row
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
# Step 1: Find all Skill tool invocations
|
|
63
|
+
skill_events = self._get_skill_events(conn, profile_id)
|
|
64
|
+
if not skill_events:
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
# Step 2: Extract skill names from input_summary
|
|
68
|
+
skill_invocations = self._parse_skill_invocations(skill_events)
|
|
69
|
+
result["skills_found"] = len(set(s["skill_name"] for s in skill_invocations))
|
|
70
|
+
|
|
71
|
+
if not skill_invocations:
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
# Step 3: Build execution traces and compute outcomes
|
|
75
|
+
skill_metrics = self._compute_skill_metrics(
|
|
76
|
+
conn, profile_id, skill_invocations,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Step 4: Create/update behavioral assertions for each skill
|
|
80
|
+
for skill_name, metrics in skill_metrics.items():
|
|
81
|
+
if metrics["total_invocations"] < MIN_INVOCATIONS:
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
r = self._upsert_skill_assertion(conn, profile_id, skill_name, metrics)
|
|
85
|
+
result[f"assertions_{r}"] = result.get(f"assertions_{r}", 0) + 1
|
|
86
|
+
|
|
87
|
+
# Step 5: Detect skill correlations (pairs used together)
|
|
88
|
+
correlations = self._detect_skill_correlations(skill_invocations)
|
|
89
|
+
for pair, corr_data in correlations.items():
|
|
90
|
+
if corr_data["count"] >= 3:
|
|
91
|
+
self._upsert_correlation_assertion(
|
|
92
|
+
conn, profile_id, pair, corr_data,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
conn.commit()
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
logger.warning("Skill performance mining failed: %s", exc)
|
|
98
|
+
result["error"] = str(exc)
|
|
99
|
+
finally:
|
|
100
|
+
conn.close()
|
|
101
|
+
|
|
102
|
+
logger.info(
|
|
103
|
+
"Skill performance mining: %d skills, %d assertions",
|
|
104
|
+
result["skills_found"],
|
|
105
|
+
result.get("assertions_created", 0) + result.get("assertions_reinforced", 0),
|
|
106
|
+
)
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
def _get_skill_events(
|
|
110
|
+
self, conn: sqlite3.Connection, profile_id: str,
|
|
111
|
+
) -> list[dict]:
|
|
112
|
+
"""Get all Skill tool events with enriched data."""
|
|
113
|
+
rows = conn.execute(
|
|
114
|
+
"SELECT id, session_id, tool_name, event_type, input_summary, "
|
|
115
|
+
"output_summary, project_path, created_at "
|
|
116
|
+
"FROM tool_events "
|
|
117
|
+
"WHERE profile_id = ? AND tool_name = 'Skill' "
|
|
118
|
+
"ORDER BY created_at ASC",
|
|
119
|
+
(profile_id,),
|
|
120
|
+
).fetchall()
|
|
121
|
+
return [dict(r) for r in rows]
|
|
122
|
+
|
|
123
|
+
def _parse_skill_invocations(self, skill_events: list[dict]) -> list[dict]:
|
|
124
|
+
"""Extract skill name and args from input_summary JSON."""
|
|
125
|
+
invocations = []
|
|
126
|
+
|
|
127
|
+
for event in skill_events:
|
|
128
|
+
input_raw = event.get("input_summary", "")
|
|
129
|
+
output_raw = event.get("output_summary", "")
|
|
130
|
+
skill_name = ""
|
|
131
|
+
|
|
132
|
+
# Try extracting from input_summary (enriched hook format)
|
|
133
|
+
if input_raw:
|
|
134
|
+
try:
|
|
135
|
+
inp = json.loads(input_raw) if input_raw.startswith("{") else {}
|
|
136
|
+
skill_name = inp.get("skill", "")
|
|
137
|
+
except (json.JSONDecodeError, TypeError):
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
# Fallback: try output_summary (ECC ingestion format)
|
|
141
|
+
if not skill_name and output_raw:
|
|
142
|
+
try:
|
|
143
|
+
out = json.loads(output_raw) if output_raw.startswith("{") else {}
|
|
144
|
+
skill_name = out.get("commandName", "")
|
|
145
|
+
except (json.JSONDecodeError, TypeError):
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
if not skill_name:
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
invocations.append({
|
|
152
|
+
"skill_name": skill_name,
|
|
153
|
+
"session_id": event.get("session_id", ""),
|
|
154
|
+
"event_id": event.get("id", 0),
|
|
155
|
+
"created_at": event.get("created_at", ""),
|
|
156
|
+
"project_path": event.get("project_path", ""),
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
return invocations
|
|
160
|
+
|
|
161
|
+
def _compute_skill_metrics(
|
|
162
|
+
self,
|
|
163
|
+
conn: sqlite3.Connection,
|
|
164
|
+
profile_id: str,
|
|
165
|
+
invocations: list[dict],
|
|
166
|
+
) -> dict[str, dict]:
|
|
167
|
+
"""Compute per-skill metrics using execution trace heuristic.
|
|
168
|
+
|
|
169
|
+
Outcome heuristic (conservative, labeled as APPROXIMATE):
|
|
170
|
+
- Signal 1 (POSITIVE): Productive tools follow (Edit, Write, Bash success)
|
|
171
|
+
- Signal 2 (NEGATIVE): Same Skill re-invoked within 5 min
|
|
172
|
+
- Signal 3 (NEGATIVE): Bash errors in next 3 events
|
|
173
|
+
- Signal 4 (WEAK POSITIVE): Session continues 10+ events
|
|
174
|
+
|
|
175
|
+
H-N1QUERY: Batch-loads all trace events in one query instead of N+1.
|
|
176
|
+
"""
|
|
177
|
+
metrics: dict[str, dict] = defaultdict(lambda: {
|
|
178
|
+
"total_invocations": 0,
|
|
179
|
+
"positive_signals": 0,
|
|
180
|
+
"negative_signals": 0,
|
|
181
|
+
"sessions": set(),
|
|
182
|
+
"projects": set(),
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
if not invocations:
|
|
186
|
+
return {}
|
|
187
|
+
|
|
188
|
+
# H-N1QUERY: Batch-load all potential trace events in one query.
|
|
189
|
+
# Find the min event_id across all invocations so we can fetch
|
|
190
|
+
# all subsequent events in a single SELECT.
|
|
191
|
+
min_event_id = min(inv["event_id"] for inv in invocations)
|
|
192
|
+
all_trace_rows = conn.execute(
|
|
193
|
+
"SELECT id, tool_name, event_type, output_summary, created_at "
|
|
194
|
+
"FROM tool_events "
|
|
195
|
+
"WHERE profile_id = ? AND id > ? "
|
|
196
|
+
"ORDER BY id ASC",
|
|
197
|
+
(profile_id, min_event_id),
|
|
198
|
+
).fetchall()
|
|
199
|
+
all_trace = [dict(r) for r in all_trace_rows]
|
|
200
|
+
|
|
201
|
+
# Build an index: for each event_id, find its position in all_trace
|
|
202
|
+
# so we can slice TRACE_WINDOW events after it in O(1).
|
|
203
|
+
trace_id_to_idx: dict[int, int] = {}
|
|
204
|
+
for idx, t in enumerate(all_trace):
|
|
205
|
+
if t["id"] not in trace_id_to_idx:
|
|
206
|
+
trace_id_to_idx[t["id"]] = idx
|
|
207
|
+
|
|
208
|
+
for inv in invocations:
|
|
209
|
+
skill = inv["skill_name"]
|
|
210
|
+
m = metrics[skill]
|
|
211
|
+
m["total_invocations"] += 1
|
|
212
|
+
m["sessions"].add(inv["session_id"])
|
|
213
|
+
if inv["project_path"]:
|
|
214
|
+
m["projects"].add(inv["project_path"])
|
|
215
|
+
|
|
216
|
+
# Find trace window for this invocation from pre-loaded data
|
|
217
|
+
# Events with id > inv["event_id"], take first TRACE_WINDOW
|
|
218
|
+
start_idx = 0
|
|
219
|
+
eid = inv["event_id"]
|
|
220
|
+
# The first entry in all_trace with id > eid
|
|
221
|
+
# Since all_trace starts at min_event_id+1 and is sorted, we
|
|
222
|
+
# can bisect or scan. Use the index if the next id is present.
|
|
223
|
+
# Simple approach: events after eid start at the position of eid+1
|
|
224
|
+
# or the first id > eid in the sorted list.
|
|
225
|
+
for candidate_id in range(eid + 1, eid + TRACE_WINDOW + 2):
|
|
226
|
+
if candidate_id in trace_id_to_idx:
|
|
227
|
+
start_idx = trace_id_to_idx[candidate_id]
|
|
228
|
+
break
|
|
229
|
+
else:
|
|
230
|
+
# No trace events found after this invocation
|
|
231
|
+
start_idx = len(all_trace)
|
|
232
|
+
|
|
233
|
+
trace_list = all_trace[start_idx:start_idx + TRACE_WINDOW]
|
|
234
|
+
outcome = self._evaluate_trace(skill, inv, trace_list, invocations)
|
|
235
|
+
|
|
236
|
+
if outcome > 0:
|
|
237
|
+
m["positive_signals"] += 1
|
|
238
|
+
elif outcome < 0:
|
|
239
|
+
m["negative_signals"] += 1
|
|
240
|
+
|
|
241
|
+
# Compute final metrics per skill
|
|
242
|
+
result = {}
|
|
243
|
+
for skill, m in metrics.items():
|
|
244
|
+
total = m["total_invocations"]
|
|
245
|
+
positive = m["positive_signals"]
|
|
246
|
+
negative = m["negative_signals"]
|
|
247
|
+
|
|
248
|
+
effective_score = (positive - negative) / total if total > 0 else 0.0
|
|
249
|
+
result[skill] = {
|
|
250
|
+
"total_invocations": total,
|
|
251
|
+
"positive_signals": positive,
|
|
252
|
+
"negative_signals": negative,
|
|
253
|
+
"effective_score": round(max(-1.0, min(1.0, effective_score)), 3),
|
|
254
|
+
"session_count": len(m["sessions"]),
|
|
255
|
+
"project_count": len(m["projects"]),
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return result
|
|
259
|
+
|
|
260
|
+
def _evaluate_trace(
|
|
261
|
+
self,
|
|
262
|
+
skill_name: str,
|
|
263
|
+
invocation: dict,
|
|
264
|
+
trace: list[dict],
|
|
265
|
+
all_invocations: list[dict],
|
|
266
|
+
) -> int:
|
|
267
|
+
"""Evaluate execution trace after a Skill call. Returns +1, 0, or -1."""
|
|
268
|
+
if not trace:
|
|
269
|
+
return 0
|
|
270
|
+
|
|
271
|
+
score = 0
|
|
272
|
+
|
|
273
|
+
# Signal 1: Productive tools in trace → +1
|
|
274
|
+
productive_tools = {"Edit", "Write"}
|
|
275
|
+
if any(t["tool_name"] in productive_tools for t in trace[:TRACE_WINDOW]):
|
|
276
|
+
score += 1
|
|
277
|
+
|
|
278
|
+
# Signal 2: Same Skill re-invoked within RETRY_WINDOW → -1
|
|
279
|
+
inv_time = invocation.get("created_at", "")
|
|
280
|
+
for other in all_invocations:
|
|
281
|
+
if other["event_id"] == invocation["event_id"]:
|
|
282
|
+
continue
|
|
283
|
+
if other["skill_name"] != skill_name:
|
|
284
|
+
continue
|
|
285
|
+
if other["session_id"] != invocation["session_id"]:
|
|
286
|
+
continue
|
|
287
|
+
try:
|
|
288
|
+
t1 = datetime.fromisoformat(inv_time.replace("Z", "+00:00"))
|
|
289
|
+
t2 = datetime.fromisoformat(
|
|
290
|
+
other["created_at"].replace("Z", "+00:00"),
|
|
291
|
+
)
|
|
292
|
+
delta = abs((t2 - t1).total_seconds())
|
|
293
|
+
if 0 < delta <= RETRY_WINDOW_SECONDS:
|
|
294
|
+
score -= 1
|
|
295
|
+
break
|
|
296
|
+
except (ValueError, TypeError):
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
# Signal 3: Bash errors in first 3 events → -1
|
|
300
|
+
for t in trace[:3]:
|
|
301
|
+
if t["tool_name"] == "Bash":
|
|
302
|
+
output = t.get("output_summary", "")
|
|
303
|
+
if output and any(
|
|
304
|
+
kw in output.lower()
|
|
305
|
+
for kw in ("error", "failed", "command not found", "permission denied")
|
|
306
|
+
):
|
|
307
|
+
score -= 1
|
|
308
|
+
break
|
|
309
|
+
|
|
310
|
+
# Clamp to [-1, +1]
|
|
311
|
+
return max(-1, min(1, score))
|
|
312
|
+
|
|
313
|
+
def _detect_skill_correlations(
|
|
314
|
+
self, invocations: list[dict],
|
|
315
|
+
) -> dict[tuple[str, str], dict]:
|
|
316
|
+
"""Find skills frequently used together in the same session."""
|
|
317
|
+
session_skills: dict[str, set[str]] = defaultdict(set)
|
|
318
|
+
for inv in invocations:
|
|
319
|
+
session_skills[inv["session_id"]].add(inv["skill_name"])
|
|
320
|
+
|
|
321
|
+
pair_counts: Counter = Counter()
|
|
322
|
+
for skills in session_skills.values():
|
|
323
|
+
skill_list = sorted(skills)
|
|
324
|
+
for i in range(len(skill_list)):
|
|
325
|
+
for j in range(i + 1, len(skill_list)):
|
|
326
|
+
pair_counts[(skill_list[i], skill_list[j])] += 1
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
pair: {"count": count, "sessions": count}
|
|
330
|
+
for pair, count in pair_counts.most_common(10)
|
|
331
|
+
if count >= 2
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
def _upsert_skill_assertion(
|
|
335
|
+
self,
|
|
336
|
+
conn: sqlite3.Connection,
|
|
337
|
+
profile_id: str,
|
|
338
|
+
skill_name: str,
|
|
339
|
+
metrics: dict,
|
|
340
|
+
) -> str:
|
|
341
|
+
"""Create or reinforce a skill performance assertion."""
|
|
342
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
343
|
+
eff = metrics["effective_score"]
|
|
344
|
+
total = metrics["total_invocations"]
|
|
345
|
+
|
|
346
|
+
trigger = f"when considering skill {skill_name}"
|
|
347
|
+
action = (
|
|
348
|
+
f"effective score: {eff:.0%} (approximate, {total} invocations, "
|
|
349
|
+
f"{metrics['session_count']} sessions)"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
assertion_id = hashlib.sha256(
|
|
353
|
+
f"{profile_id}:skill_perf:{skill_name}".encode(),
|
|
354
|
+
).hexdigest()[:32]
|
|
355
|
+
|
|
356
|
+
existing = conn.execute(
|
|
357
|
+
"SELECT id, confidence FROM behavioral_assertions WHERE id = ?",
|
|
358
|
+
(assertion_id,),
|
|
359
|
+
).fetchone()
|
|
360
|
+
|
|
361
|
+
confidence = min(0.85, max(0.3, abs(eff) * 0.8 + total / 100))
|
|
362
|
+
|
|
363
|
+
if existing:
|
|
364
|
+
old_conf = dict(existing)["confidence"]
|
|
365
|
+
new_conf = old_conf + (1.0 - old_conf) * REINFORCEMENT_NUDGE
|
|
366
|
+
conn.execute(
|
|
367
|
+
"UPDATE behavioral_assertions SET "
|
|
368
|
+
"action = ?, confidence = ?, evidence_count = ?, "
|
|
369
|
+
"reinforcement_count = reinforcement_count + 1, "
|
|
370
|
+
"last_reinforced_at = ?, updated_at = ? WHERE id = ?",
|
|
371
|
+
(action, round(min(0.95, new_conf), 4), total, now, now, assertion_id),
|
|
372
|
+
)
|
|
373
|
+
return "reinforced"
|
|
374
|
+
else:
|
|
375
|
+
conn.execute(
|
|
376
|
+
"INSERT INTO behavioral_assertions "
|
|
377
|
+
"(id, profile_id, project_path, trigger_condition, action, "
|
|
378
|
+
" category, confidence, evidence_count, source, created_at, updated_at) "
|
|
379
|
+
"VALUES (?, ?, '', ?, ?, 'skill_performance', ?, ?, 'skill_miner', ?, ?)",
|
|
380
|
+
(assertion_id, profile_id, trigger, action,
|
|
381
|
+
round(confidence, 4), total, now, now),
|
|
382
|
+
)
|
|
383
|
+
return "created"
|
|
384
|
+
|
|
385
|
+
def _upsert_correlation_assertion(
|
|
386
|
+
self,
|
|
387
|
+
conn: sqlite3.Connection,
|
|
388
|
+
profile_id: str,
|
|
389
|
+
pair: tuple[str, str],
|
|
390
|
+
corr_data: dict,
|
|
391
|
+
) -> None:
|
|
392
|
+
"""Create assertion for skill correlation."""
|
|
393
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
394
|
+
trigger = f"when using {pair[0]}"
|
|
395
|
+
action = f"often paired with {pair[1]} ({corr_data['count']} sessions together)"
|
|
396
|
+
|
|
397
|
+
assertion_id = hashlib.sha256(
|
|
398
|
+
f"{profile_id}:skill_corr:{pair[0]}:{pair[1]}".encode(),
|
|
399
|
+
).hexdigest()[:32]
|
|
400
|
+
|
|
401
|
+
existing = conn.execute(
|
|
402
|
+
"SELECT id FROM behavioral_assertions WHERE id = ?",
|
|
403
|
+
(assertion_id,),
|
|
404
|
+
).fetchone()
|
|
405
|
+
|
|
406
|
+
if existing:
|
|
407
|
+
conn.execute(
|
|
408
|
+
"UPDATE behavioral_assertions SET "
|
|
409
|
+
"action = ?, reinforcement_count = reinforcement_count + 1, "
|
|
410
|
+
"last_reinforced_at = ?, updated_at = ? WHERE id = ?",
|
|
411
|
+
(action, now, now, assertion_id),
|
|
412
|
+
)
|
|
413
|
+
else:
|
|
414
|
+
conn.execute(
|
|
415
|
+
"INSERT INTO behavioral_assertions "
|
|
416
|
+
"(id, profile_id, project_path, trigger_condition, action, "
|
|
417
|
+
" category, confidence, evidence_count, source, created_at, updated_at) "
|
|
418
|
+
"VALUES (?, ?, '', ?, ?, 'skill_correlation', ?, ?, 'skill_miner', ?, ?)",
|
|
419
|
+
(assertion_id, profile_id, trigger, action,
|
|
420
|
+
round(min(0.7, corr_data["count"] / 10), 4),
|
|
421
|
+
corr_data["count"], now, now),
|
|
422
|
+
)
|
|
@@ -79,6 +79,8 @@ _ESSENTIAL_TOOLS: set[str] = {
|
|
|
79
79
|
# v3.4.7: Two-way learning (4)
|
|
80
80
|
"log_tool_event", "get_assertions",
|
|
81
81
|
"reinforce_assertion", "contradict_assertion",
|
|
82
|
+
# v3.4.11: Skill evolution (3)
|
|
83
|
+
"evolve_skill", "skill_health", "skill_lineage",
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
# v3.4.4: Mesh tools — enabled if mesh_enabled in config or SLM_MCP_MESH_TOOLS=1
|
|
@@ -138,6 +140,7 @@ from superlocalmemory.mcp.resources import register_resources
|
|
|
138
140
|
from superlocalmemory.mcp.tools_code_graph import register_code_graph_tools
|
|
139
141
|
from superlocalmemory.mcp.tools_mesh import register_mesh_tools
|
|
140
142
|
from superlocalmemory.mcp.tools_learning import register_learning_tools
|
|
143
|
+
from superlocalmemory.mcp.tools_evolution import register_evolution_tools
|
|
141
144
|
|
|
142
145
|
register_core_tools(_target, get_engine)
|
|
143
146
|
register_v28_tools(_target, get_engine)
|
|
@@ -148,6 +151,7 @@ register_resources(server, get_engine) # Resources always registered (not tools
|
|
|
148
151
|
register_code_graph_tools(_target, get_engine) # CodeGraph: filtered like other tools (SLM_MCP_ALL_TOOLS=1 to show all)
|
|
149
152
|
register_mesh_tools(_target, get_engine) # v3.4.4: Mesh P2P tools — ships with SLM, no separate slm-mesh needed
|
|
150
153
|
register_learning_tools(_target, get_engine) # v3.4.7: Two-way learning tools
|
|
154
|
+
register_evolution_tools(_target, get_engine) # v3.4.11: Skill evolution tools
|
|
151
155
|
|
|
152
156
|
|
|
153
157
|
# V3.3.21: Eager engine warmup — start initializing BEFORE first tool call.
|