@cachly-dev/mcp-server 0.10.49 → 0.10.51
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/BENCH.md +80 -0
- package/STRATEGY.md +33 -14
- package/apps/travel-chaos-organizer/frontend/package-lock.json +184 -0
- package/dist/src/__tests__/rerank.test.d.ts +1 -0
- package/dist/src/__tests__/rerank.test.js +115 -0
- package/dist/src/__tests__/rerank.test.js.map +1 -0
- package/dist/src/bench/cachly-bench.d.ts +35 -0
- package/dist/src/bench/cachly-bench.js +182 -0
- package/dist/src/bench/cachly-bench.js.map +1 -0
- package/dist/src/bench/fixtures.d.ts +34 -0
- package/dist/src/bench/fixtures.js +199 -0
- package/dist/src/bench/fixtures.js.map +1 -0
- package/dist/src/handlers/brain.js +141 -65
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/handlers/team.js +56 -4
- package/dist/src/handlers/team.js.map +1 -1
- package/dist/src/rerank.d.ts +55 -0
- package/dist/src/rerank.js +111 -0
- package/dist/src/rerank.js.map +1 -0
- package/dist/src/tools.d.ts +30 -0
- package/dist/src/tools.js +18 -0
- package/dist/src/tools.js.map +1 -1
- package/package.json +2 -1
- package/src/__tests__/rerank.test.ts +130 -0
- package/src/bench/cachly-bench.ts +217 -0
- package/src/bench/fixtures.ts +226 -0
- package/src/handlers/brain.ts +156 -73
- package/src/handlers/team.ts +66 -4
- package/src/rerank.ts +148 -0
- package/src/tools.ts +19 -0
package/src/rerank.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality-aware reranking for Brain recall.
|
|
3
|
+
*
|
|
4
|
+
* The raw BM25+ keyword search (src/search.ts) ranks purely by *textual* relevance.
|
|
5
|
+
* That is necessary but not sufficient: when an agent asks "how do I fix the flaky
|
|
6
|
+
* deploy?", a *failed* attempt with similar words can out-rank the *proven success*
|
|
7
|
+
* lesson that actually solves it. Anthropic's flat-file memory has exactly this
|
|
8
|
+
* problem — it reads files in order, with no notion of which lesson is trustworthy.
|
|
9
|
+
*
|
|
10
|
+
* This module is Cachly's structural advantage made concrete: we re-rank text
|
|
11
|
+
* matches by the *quality* of the underlying lesson — outcome, confidence,
|
|
12
|
+
* proven-ness (recall_count), severity and human review (endorsements) — so the
|
|
13
|
+
* lesson most likely to help surfaces first. A flat-file memory has none of these
|
|
14
|
+
* signals; a senior-confirmed, ten-times-recalled success reads identically to a
|
|
15
|
+
* one-off failed attempt.
|
|
16
|
+
*
|
|
17
|
+
* It is intentionally pure (no I/O): the lesson JSON already travels inside each
|
|
18
|
+
* KeywordMatch.content for `cachly:lesson:best:*` keys, so reranking needs no extra
|
|
19
|
+
* Redis round-trips. This keeps it fast and trivially benchmarkable (see
|
|
20
|
+
* src/bench/cachly-bench.ts).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { KeywordMatch } from './search.js';
|
|
24
|
+
import { safeJsonParse } from './utils.js';
|
|
25
|
+
|
|
26
|
+
/** Subset of a stored lesson that influences ranking. */
|
|
27
|
+
export interface LessonQuality {
|
|
28
|
+
outcome?: string; // 'success' | 'partial' | 'failure'
|
|
29
|
+
confidence?: number; // 0..1
|
|
30
|
+
recall_count?: number; // times this lesson has been recalled (proven-ness)
|
|
31
|
+
severity?: string; // 'critical' | 'major' | 'minor'
|
|
32
|
+
ts?: string; // ISO timestamp (for freshness, mild)
|
|
33
|
+
verified_at?: string;
|
|
34
|
+
// ── Governance (Phase 3): human review raises trust above mere usage ──
|
|
35
|
+
reviewed_by?: string; // handle of the reviewer who endorsed it (latest)
|
|
36
|
+
review_level?: string; // 'senior' | 'peer' (weight differs)
|
|
37
|
+
endorsements?: number; // distinct human endorsements
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RerankedMatch extends KeywordMatch {
|
|
41
|
+
/** Final score after applying the quality multiplier. */
|
|
42
|
+
finalScore: number;
|
|
43
|
+
/** The multiplier that was applied to the BM25 score (1.0 = neutral). */
|
|
44
|
+
qualityBoost: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Tunable weights (kept conservative; validated by Cachly-Bench) ─────────────
|
|
48
|
+
const OUTCOME_WEIGHT: Record<string, number> = {
|
|
49
|
+
success: 1.0,
|
|
50
|
+
partial: 0.8,
|
|
51
|
+
failure: 0.55,
|
|
52
|
+
};
|
|
53
|
+
const OUTCOME_DEFAULT = 0.75; // unknown / not a lesson with an outcome
|
|
54
|
+
|
|
55
|
+
const SEVERITY_WEIGHT: Record<string, number> = {
|
|
56
|
+
critical: 1.15,
|
|
57
|
+
major: 1.05,
|
|
58
|
+
minor: 1.0,
|
|
59
|
+
};
|
|
60
|
+
const SEVERITY_DEFAULT = 1.0;
|
|
61
|
+
|
|
62
|
+
// Governance: a human-confirmed lesson is more trustworthy than one proven only
|
|
63
|
+
// by automated recall. A senior review weighs more than a peer review; multiple
|
|
64
|
+
// endorsements add a small, saturating bump on top.
|
|
65
|
+
const REVIEW_LEVEL_WEIGHT: Record<string, number> = {
|
|
66
|
+
senior: 1.25,
|
|
67
|
+
peer: 1.1,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Clamp so a strong text match is never fully buried by quality, and a weak text
|
|
71
|
+
// match is never catapulted to the top purely on quality.
|
|
72
|
+
const MIN_MULTIPLIER = 0.5;
|
|
73
|
+
const MAX_MULTIPLIER = 2.5;
|
|
74
|
+
|
|
75
|
+
const LESSON_KEY_PREFIX = 'cachly:lesson:best:';
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Compute the quality multiplier for a single lesson.
|
|
79
|
+
* Returns 1.0 (neutral) for documents that carry no quality signal (e.g. context).
|
|
80
|
+
*/
|
|
81
|
+
export function qualityMultiplier(lesson: LessonQuality | null): number {
|
|
82
|
+
if (!lesson || (lesson.outcome === undefined && lesson.confidence === undefined
|
|
83
|
+
&& lesson.recall_count === undefined && lesson.reviewed_by === undefined
|
|
84
|
+
&& lesson.endorsements === undefined)) {
|
|
85
|
+
return 1.0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 1. Outcome — a proven success is what the agent actually wants.
|
|
89
|
+
const outcome = OUTCOME_WEIGHT[lesson.outcome ?? ''] ?? OUTCOME_DEFAULT;
|
|
90
|
+
|
|
91
|
+
// 2. Confidence — maps 0..1 → 0.7..1.3 so it nudges, not dominates.
|
|
92
|
+
const conf = typeof lesson.confidence === 'number' ? Math.max(0, Math.min(1, lesson.confidence)) : 0.6;
|
|
93
|
+
const confidenceFactor = 0.7 + 0.6 * conf;
|
|
94
|
+
|
|
95
|
+
// 3. Proven-ness — a lesson recalled many times has stood the test of use.
|
|
96
|
+
// Log-scaled so it saturates: rc=0→1.0, rc=5→~1.18, rc=50→~1.30.
|
|
97
|
+
const rc = Math.max(0, lesson.recall_count ?? 0);
|
|
98
|
+
const provenBoost = 1 + Math.min(0.3, Math.log1p(rc) / 13);
|
|
99
|
+
|
|
100
|
+
// 4. Severity — when two lessons tie, surface the one guarding the worse failure.
|
|
101
|
+
const severity = SEVERITY_WEIGHT[lesson.severity ?? ''] ?? SEVERITY_DEFAULT;
|
|
102
|
+
|
|
103
|
+
// 5. Governance — a human-confirmed lesson outranks an unreviewed one.
|
|
104
|
+
// Base weight from the review level, plus a small saturating bump for
|
|
105
|
+
// additional endorsements (rc-style log curve, capped at +0.1).
|
|
106
|
+
const reviewBase = lesson.reviewed_by
|
|
107
|
+
? (REVIEW_LEVEL_WEIGHT[lesson.review_level ?? 'peer'] ?? REVIEW_LEVEL_WEIGHT.peer)
|
|
108
|
+
: 1.0;
|
|
109
|
+
const extraEndorse = Math.max(0, (lesson.endorsements ?? 0) - 1);
|
|
110
|
+
const reviewBoost = reviewBase * (1 + Math.min(0.1, Math.log1p(extraEndorse) / 20));
|
|
111
|
+
|
|
112
|
+
const m = outcome * confidenceFactor * provenBoost * severity * reviewBoost;
|
|
113
|
+
return Math.max(MIN_MULTIPLIER, Math.min(MAX_MULTIPLIER, m));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Extract the quality signal from a match, if its content is a stored lesson. */
|
|
117
|
+
export function extractLessonQuality(match: Pick<KeywordMatch, 'key' | 'content'>): LessonQuality | null {
|
|
118
|
+
if (!match.key.startsWith(LESSON_KEY_PREFIX)) return null;
|
|
119
|
+
return safeJsonParse<LessonQuality | null>(match.content, null);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Rerank BM25 keyword matches by lesson quality.
|
|
124
|
+
*
|
|
125
|
+
* The BM25 scores are first min-max normalized to [0,1] so the quality multiplier
|
|
126
|
+
* acts on a comparable scale across queries; ties are broken by the original BM25
|
|
127
|
+
* score (stable, deterministic).
|
|
128
|
+
*/
|
|
129
|
+
export function rerankByQuality(matches: KeywordMatch[]): RerankedMatch[] {
|
|
130
|
+
if (matches.length === 0) return [];
|
|
131
|
+
|
|
132
|
+
const maxScore = Math.max(...matches.map(m => m.score));
|
|
133
|
+
const minScore = Math.min(...matches.map(m => m.score));
|
|
134
|
+
const range = maxScore - minScore || 1;
|
|
135
|
+
|
|
136
|
+
const reranked: RerankedMatch[] = matches.map(m => {
|
|
137
|
+
const norm = (m.score - minScore) / range; // 0..1
|
|
138
|
+
const lesson = extractLessonQuality(m);
|
|
139
|
+
const boost = qualityMultiplier(lesson);
|
|
140
|
+
// Blend: keep a floor of raw relevance so a top text hit stays competitive,
|
|
141
|
+
// then layer the quality multiplier on the normalized component.
|
|
142
|
+
const finalScore = m.score * (0.4 + 0.6 * boost) + norm * (boost - 1) * 0.0001;
|
|
143
|
+
return { ...m, finalScore, qualityBoost: boost };
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
reranked.sort((a, b) => (b.finalScore - a.finalScore) || (b.score - a.score));
|
|
147
|
+
return reranked;
|
|
148
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -840,6 +840,25 @@ const TOOLS = [
|
|
|
840
840
|
required: ['instance_id', 'author', 'topic', 'outcome', 'what_worked'],
|
|
841
841
|
},
|
|
842
842
|
},
|
|
843
|
+
{
|
|
844
|
+
name: 'team_confirm',
|
|
845
|
+
description:
|
|
846
|
+
'Endorse (review-confirm) a team lesson so trusted, human-reviewed knowledge ranks above unreviewed auto-learned entries. ' +
|
|
847
|
+
'A senior review weighs more than a peer review; distinct endorsements add a small boost. ' +
|
|
848
|
+
'Confirmed lessons surface higher in smart_recall and team_recall and carry a 🛡️/✔️ badge. ' +
|
|
849
|
+
'Use this in code review or knowledge reviews to bless the canonical solution for a topic.',
|
|
850
|
+
inputSchema: {
|
|
851
|
+
type: 'object',
|
|
852
|
+
properties: {
|
|
853
|
+
instance_id: { type: 'string', description: 'UUID of the shared team brain instance' },
|
|
854
|
+
topic: { type: 'string', description: 'Topic slug of the lesson to confirm (e.g. "deploy:api")' },
|
|
855
|
+
reviewer: { type: 'string', description: 'Your name or handle (the reviewer endorsing this lesson)' },
|
|
856
|
+
level: { type: 'string', enum: ['senior', 'peer'], description: 'Review weight — "senior" ranks higher than "peer" (default: peer)' },
|
|
857
|
+
note: { type: 'string', description: 'Optional review note (kept in the lesson audit trail)' },
|
|
858
|
+
},
|
|
859
|
+
required: ['instance_id', 'topic', 'reviewer'],
|
|
860
|
+
},
|
|
861
|
+
},
|
|
843
862
|
{
|
|
844
863
|
name: 'team_recall',
|
|
845
864
|
description:
|