@kage-core/kage-graph-mcp 3.1.0 → 3.2.0
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/dist/kernel.js +90 -91
- package/dist/metrics-math.js +71 -0
- package/package.json +2 -2
- package/viewer/console.js +3 -11
- package/viewer/index.html +1 -3
package/dist/kernel.js
CHANGED
|
@@ -154,6 +154,7 @@ exports.setupAgent = setupAgent;
|
|
|
154
154
|
exports.generatePluginHooks = generatePluginHooks;
|
|
155
155
|
exports.setupDoctor = setupDoctor;
|
|
156
156
|
exports.verifyAgentActivation = verifyAgentActivation;
|
|
157
|
+
exports.pruneObservations = pruneObservations;
|
|
157
158
|
exports.observe = observe;
|
|
158
159
|
exports.observationSignalScore = observationSignalScore;
|
|
159
160
|
exports.kageSessionCaptureReport = kageSessionCaptureReport;
|
|
@@ -209,6 +210,7 @@ const node_worker_threads_1 = require("node:worker_threads");
|
|
|
209
210
|
const ts = __importStar(require("typescript"));
|
|
210
211
|
const index_js_1 = require("./registry/index.js");
|
|
211
212
|
const okf_js_1 = require("./okf.js");
|
|
213
|
+
const metrics_math_js_1 = require("./metrics-math.js");
|
|
212
214
|
exports.PACKET_SCHEMA_VERSION = 2;
|
|
213
215
|
exports.MEMORY_TYPES = [
|
|
214
216
|
"repo_map",
|
|
@@ -375,25 +377,32 @@ For normal coding tasks:
|
|
|
375
377
|
For quick factual questions, \`kage_context\` alone is enough. For status or demo requests, call \`kage_metrics\`.
|
|
376
378
|
${AGENTS_POLICY_END}
|
|
377
379
|
`;
|
|
380
|
+
// Hooks pass raw user prompts as recall queries ("what is X? can we replace it"),
|
|
381
|
+
// so interrogatives, pronouns, and auxiliaries must be stopwords too — otherwise
|
|
382
|
+
// filler words collect BM25/vector/graph credit and drown the query's rare,
|
|
383
|
+
// high-IDF terms (a packet literally titled with the queried term lost to
|
|
384
|
+
// packets matching only "what"/"can"/"we").
|
|
378
385
|
const STOPWORDS = new Set([
|
|
379
|
-
"a",
|
|
380
|
-
"
|
|
381
|
-
"
|
|
382
|
-
"
|
|
383
|
-
"
|
|
384
|
-
"
|
|
385
|
-
"
|
|
386
|
-
"
|
|
387
|
-
"
|
|
388
|
-
"
|
|
389
|
-
"
|
|
390
|
-
"
|
|
391
|
-
"of",
|
|
392
|
-
"
|
|
393
|
-
"
|
|
394
|
-
"the",
|
|
395
|
-
"
|
|
396
|
-
"
|
|
386
|
+
"a", "about", "again", "also", "an", "and", "are", "as", "at",
|
|
387
|
+
"be", "been", "being", "but", "by",
|
|
388
|
+
"can", "could",
|
|
389
|
+
"did", "do", "does",
|
|
390
|
+
"else",
|
|
391
|
+
"for", "from",
|
|
392
|
+
"had", "has", "have", "having", "he", "her", "hers", "here", "him", "his", "how",
|
|
393
|
+
"i", "if", "in", "into", "is", "it", "its",
|
|
394
|
+
"just",
|
|
395
|
+
"let", "lets",
|
|
396
|
+
"may", "me", "might", "mine", "my",
|
|
397
|
+
"no", "not",
|
|
398
|
+
"of", "on", "once", "or", "our", "ours", "over",
|
|
399
|
+
"please",
|
|
400
|
+
"shall", "she", "should", "so",
|
|
401
|
+
"than", "that", "the", "their", "theirs", "them", "then", "there", "these", "they", "this", "those", "to", "too",
|
|
402
|
+
"under", "us",
|
|
403
|
+
"very",
|
|
404
|
+
"was", "we", "were", "what", "when", "where", "which", "who", "whom", "whose", "why", "will", "would", "with",
|
|
405
|
+
"you", "your", "yours",
|
|
397
406
|
]);
|
|
398
407
|
function memoryRoot(projectDir) {
|
|
399
408
|
return (0, node_path_1.join)(projectDir, ".agent_memory");
|
|
@@ -7010,6 +7019,7 @@ function refreshProject(projectDir, options = {}) {
|
|
|
7010
7019
|
}
|
|
7011
7020
|
const validation = validateProject(projectDir);
|
|
7012
7021
|
const metrics = kageMetricsShallow(projectDir, { codeGraph, knowledgeGraph, validation });
|
|
7022
|
+
pruneObservations(projectDir);
|
|
7013
7023
|
ensureDir(reportsDir(projectDir));
|
|
7014
7024
|
writeJson((0, node_path_1.join)(reportsDir(projectDir), "context-slots.json"), kageContextSlots(projectDir));
|
|
7015
7025
|
writeJson((0, node_path_1.join)(reportsDir(projectDir), "handoff.json"), kageMemoryHandoff(projectDir));
|
|
@@ -7943,12 +7953,17 @@ function recallBreakdown(projectDir, terms, packet, textScore, temporalScore = 0
|
|
|
7943
7953
|
const vector = Number(vectorScore.toFixed(2));
|
|
7944
7954
|
const usage = Number(usageScore.toFixed(2));
|
|
7945
7955
|
const pathTypeTagWeight = packet.type === "reference" ? 0.2 : 0.8;
|
|
7946
|
-
//
|
|
7947
|
-
// lexical/semantic/graph/intent match to the top — that is what
|
|
7948
|
-
// off-domain junk
|
|
7956
|
+
// Priors (usage, quality, freshness) must only AMPLIFY genuine relevance, never float a
|
|
7957
|
+
// packet that has no lexical/semantic/graph/tag/intent match to the top — that is what
|
|
7958
|
+
// produced confident off-domain junk: a hot, high-quality packet ranked above the one
|
|
7959
|
+
// packet whose title literally contained the queried term, because its unconditional
|
|
7960
|
+
// quality+freshness (~12 pts) beat a weak-but-real lexical match.
|
|
7949
7961
|
const coreRelevance = textScore + graphScore + intent + vector;
|
|
7962
|
+
const matchSignal = coreRelevance + pathTypeTag;
|
|
7950
7963
|
const effectiveUsage = coreRelevance > 0 ? usage : 0;
|
|
7951
|
-
const
|
|
7964
|
+
const effectiveQuality = matchSignal > 0 ? quality : 0;
|
|
7965
|
+
const effectiveFreshness = matchSignal > 0 ? freshness : Math.min(freshness, 0);
|
|
7966
|
+
const final = Number((textScore + graphScore + pathTypeTag * pathTypeTagWeight + intent + vector + effectiveUsage + effectiveFreshness + effectiveQuality + feedback).toFixed(2));
|
|
7952
7967
|
return {
|
|
7953
7968
|
bm25: textScore,
|
|
7954
7969
|
text: textScore,
|
|
@@ -12987,16 +13002,16 @@ function benchmarkCodingMemoryQuality(options = {}) {
|
|
|
12987
13002
|
retrieved,
|
|
12988
13003
|
latency_ms: latencyMs,
|
|
12989
13004
|
context_tokens: estimateTokens(recalled.context_block),
|
|
12990
|
-
recall: Object.fromEntries(metricsK.map((k) => [`at_${k}`, roundDecimal(codingRecallAt(retrieved, relevant, k) * 100, 2)])),
|
|
12991
|
-
precision_at_5_percent: roundDecimal(codingPrecisionAt(retrieved, relevant, 5) * 100, 2),
|
|
12992
|
-
ndcg_at_10: roundDecimal(codingNdcgAt(retrieved, relevant, 10), 4),
|
|
12993
|
-
mrr: roundDecimal(codingMrr(retrieved, relevant), 4),
|
|
13005
|
+
recall: Object.fromEntries(metricsK.map((k) => [`at_${k}`, (0, metrics_math_js_1.roundDecimal)((0, metrics_math_js_1.codingRecallAt)(retrieved, relevant, k) * 100, 2)])),
|
|
13006
|
+
precision_at_5_percent: (0, metrics_math_js_1.roundDecimal)((0, metrics_math_js_1.codingPrecisionAt)(retrieved, relevant, 5) * 100, 2),
|
|
13007
|
+
ndcg_at_10: (0, metrics_math_js_1.roundDecimal)((0, metrics_math_js_1.codingNdcgAt)(retrieved, relevant, 10), 4),
|
|
13008
|
+
mrr: (0, metrics_math_js_1.roundDecimal)((0, metrics_math_js_1.codingMrr)(retrieved, relevant), 4),
|
|
12994
13009
|
};
|
|
12995
13010
|
});
|
|
12996
13011
|
const sourceDiversity = codingMemorySourceDiversityProbe((0, node_path_1.join)(runDir, "source-diversity"));
|
|
12997
13012
|
const allMemoryTokens = estimateTokens(loadApprovedPackets(projectDir).map(packetText).join("\n\n"));
|
|
12998
|
-
const averageContextTokens = Math.round(averageNumber(perQuery.map((item) => item.context_tokens)));
|
|
12999
|
-
const recallByK = Object.fromEntries(metricsK.map((k) => [`recall_at_${k}_percent`, roundDecimal(averageNumber(perQuery.map((item) => item.recall[`at_${k}`] ?? 0)), 2)]));
|
|
13013
|
+
const averageContextTokens = Math.round((0, metrics_math_js_1.averageNumber)(perQuery.map((item) => item.context_tokens)));
|
|
13014
|
+
const recallByK = Object.fromEntries(metricsK.map((k) => [`recall_at_${k}_percent`, (0, metrics_math_js_1.roundDecimal)((0, metrics_math_js_1.averageNumber)(perQuery.map((item) => item.recall[`at_${k}`] ?? 0)), 2)]));
|
|
13000
13015
|
const summary = {
|
|
13001
13016
|
benchmark: "Kage coding memory quality",
|
|
13002
13017
|
retrieval_mode: "kage-recall-default",
|
|
@@ -13006,14 +13021,14 @@ function benchmarkCodingMemoryQuality(options = {}) {
|
|
|
13006
13021
|
refresh_ms: refreshMs,
|
|
13007
13022
|
...recallByK,
|
|
13008
13023
|
recall_at_k_percent: Number(recallByK[`recall_at_${topK}_percent`] ?? 0),
|
|
13009
|
-
precision_at_5_percent: roundDecimal(averageNumber(perQuery.map((item) => item.precision_at_5_percent)), 2),
|
|
13010
|
-
ndcg_at_10: roundDecimal(averageNumber(perQuery.map((item) => item.ndcg_at_10)), 4),
|
|
13011
|
-
mrr: roundDecimal(averageNumber(perQuery.map((item) => item.mrr)), 4),
|
|
13012
|
-
median_latency_ms: percentileNumber(perQuery.map((item) => item.latency_ms), 0.5),
|
|
13013
|
-
p95_latency_ms: percentileNumber(perQuery.map((item) => item.latency_ms), 0.95),
|
|
13024
|
+
precision_at_5_percent: (0, metrics_math_js_1.roundDecimal)((0, metrics_math_js_1.averageNumber)(perQuery.map((item) => item.precision_at_5_percent)), 2),
|
|
13025
|
+
ndcg_at_10: (0, metrics_math_js_1.roundDecimal)((0, metrics_math_js_1.averageNumber)(perQuery.map((item) => item.ndcg_at_10)), 4),
|
|
13026
|
+
mrr: (0, metrics_math_js_1.roundDecimal)((0, metrics_math_js_1.averageNumber)(perQuery.map((item) => item.mrr)), 4),
|
|
13027
|
+
median_latency_ms: (0, metrics_math_js_1.percentileNumber)(perQuery.map((item) => item.latency_ms), 0.5),
|
|
13028
|
+
p95_latency_ms: (0, metrics_math_js_1.percentileNumber)(perQuery.map((item) => item.latency_ms), 0.95),
|
|
13014
13029
|
all_memory_tokens: allMemoryTokens,
|
|
13015
13030
|
average_context_tokens: averageContextTokens,
|
|
13016
|
-
context_reduction_percent: roundDecimal(((allMemoryTokens - averageContextTokens) / Math.max(1, allMemoryTokens)) * 100, 2),
|
|
13031
|
+
context_reduction_percent: (0, metrics_math_js_1.roundDecimal)(((allMemoryTokens - averageContextTokens) / Math.max(1, allMemoryTokens)) * 100, 2),
|
|
13017
13032
|
source_diversity_pass: sourceDiversity.pass,
|
|
13018
13033
|
source_diversity_unique_sources: sourceDiversity.unique_sources,
|
|
13019
13034
|
source_diversity_max_results_from_one_source: sourceDiversity.max_results_from_one_source,
|
|
@@ -13027,7 +13042,7 @@ function benchmarkCodingMemoryQuality(options = {}) {
|
|
|
13027
13042
|
queries: queries.length,
|
|
13028
13043
|
packets_per_topic: packetsPerTopic,
|
|
13029
13044
|
distractors_per_topic: distractorsPerTopic,
|
|
13030
|
-
categories: countByKey(queries, (item) => item.category),
|
|
13045
|
+
categories: (0, metrics_math_js_1.countByKey)(queries, (item) => item.category),
|
|
13031
13046
|
},
|
|
13032
13047
|
top_k: topK,
|
|
13033
13048
|
metrics_k: metricsK,
|
|
@@ -13120,7 +13135,7 @@ function codingMemorySourceDiversityProbe(projectDir) {
|
|
|
13120
13135
|
source,
|
|
13121
13136
|
};
|
|
13122
13137
|
});
|
|
13123
|
-
const sourceCounts = countByKey(retrieved, (item) => item.source);
|
|
13138
|
+
const sourceCounts = (0, metrics_math_js_1.countByKey)(retrieved, (item) => item.source);
|
|
13124
13139
|
const maxResultsFromOneSource = Math.max(0, ...Object.values(sourceCounts));
|
|
13125
13140
|
const independentRank = retrieved.find((item) => item.source === "session:independent-session")?.rank ?? null;
|
|
13126
13141
|
return {
|
|
@@ -13178,16 +13193,16 @@ function benchmarkMemoryScale(options = {}) {
|
|
|
13178
13193
|
};
|
|
13179
13194
|
});
|
|
13180
13195
|
const allMemoryTokens = estimateTokens(loadApprovedPackets(projectDir).map(packetText).join("\n\n"));
|
|
13181
|
-
const averageContextTokens = Math.round(averageNumber(queries.map((item) => item.context_tokens)));
|
|
13196
|
+
const averageContextTokens = Math.round((0, metrics_math_js_1.averageNumber)(queries.map((item) => item.context_tokens)));
|
|
13182
13197
|
results.push({
|
|
13183
13198
|
packets: size,
|
|
13184
13199
|
refresh_ms: refreshMs,
|
|
13185
|
-
recall_hit_rate_percent: roundDecimal((queries.filter((item) => item.hit).length / queries.length) * 100, 2),
|
|
13186
|
-
median_recall_latency_ms: percentileNumber(queries.map((item) => item.latency_ms), 0.5),
|
|
13187
|
-
p95_recall_latency_ms: percentileNumber(queries.map((item) => item.latency_ms), 0.95),
|
|
13200
|
+
recall_hit_rate_percent: (0, metrics_math_js_1.roundDecimal)((queries.filter((item) => item.hit).length / queries.length) * 100, 2),
|
|
13201
|
+
median_recall_latency_ms: (0, metrics_math_js_1.percentileNumber)(queries.map((item) => item.latency_ms), 0.5),
|
|
13202
|
+
p95_recall_latency_ms: (0, metrics_math_js_1.percentileNumber)(queries.map((item) => item.latency_ms), 0.95),
|
|
13188
13203
|
all_memory_tokens: allMemoryTokens,
|
|
13189
13204
|
average_context_tokens: averageContextTokens,
|
|
13190
|
-
context_reduction_percent: roundDecimal(((allMemoryTokens - averageContextTokens) / Math.max(1, allMemoryTokens)) * 100, 2),
|
|
13205
|
+
context_reduction_percent: (0, metrics_math_js_1.roundDecimal)(((allMemoryTokens - averageContextTokens) / Math.max(1, allMemoryTokens)) * 100, 2),
|
|
13191
13206
|
queries,
|
|
13192
13207
|
});
|
|
13193
13208
|
}
|
|
@@ -13319,7 +13334,7 @@ function codingObservation(index, item, target, variant) {
|
|
|
13319
13334
|
topic: item.id,
|
|
13320
13335
|
target,
|
|
13321
13336
|
category: item.category,
|
|
13322
|
-
title: `${titleCase(item.query)} repo memory ${variant + 1}`,
|
|
13337
|
+
title: `${(0, metrics_math_js_1.titleCase)(item.query)} repo memory ${variant + 1}`,
|
|
13323
13338
|
summary: `Reusable learning about ${item.query}: ${item.lesson}`,
|
|
13324
13339
|
body: [
|
|
13325
13340
|
`During a real agent session, this durable repo learning was captured for ${item.query}.`,
|
|
@@ -13404,32 +13419,11 @@ function codingQualityByCategory(perQuery, metricsK) {
|
|
|
13404
13419
|
return Array.from(groups.entries()).map(([category, rows]) => ({
|
|
13405
13420
|
category,
|
|
13406
13421
|
queries: rows.length,
|
|
13407
|
-
...Object.fromEntries(metricsK.map((k) => [`recall_at_${k}_percent`, roundDecimal(averageNumber(rows.map((item) => item.recall[`at_${k}`] ?? 0)), 2)])),
|
|
13408
|
-
ndcg_at_10: roundDecimal(averageNumber(rows.map((item) => item.ndcg_at_10)), 4),
|
|
13409
|
-
mrr: roundDecimal(averageNumber(rows.map((item) => item.mrr)), 4),
|
|
13422
|
+
...Object.fromEntries(metricsK.map((k) => [`recall_at_${k}_percent`, (0, metrics_math_js_1.roundDecimal)((0, metrics_math_js_1.averageNumber)(rows.map((item) => item.recall[`at_${k}`] ?? 0)), 2)])),
|
|
13423
|
+
ndcg_at_10: (0, metrics_math_js_1.roundDecimal)((0, metrics_math_js_1.averageNumber)(rows.map((item) => item.ndcg_at_10)), 4),
|
|
13424
|
+
mrr: (0, metrics_math_js_1.roundDecimal)((0, metrics_math_js_1.averageNumber)(rows.map((item) => item.mrr)), 4),
|
|
13410
13425
|
}));
|
|
13411
13426
|
}
|
|
13412
|
-
function codingRecallAt(retrieved, relevant, k) {
|
|
13413
|
-
if (!relevant.size)
|
|
13414
|
-
return 0;
|
|
13415
|
-
return retrieved.slice(0, k).filter((item) => relevant.has(item.packet_id)).length / relevant.size;
|
|
13416
|
-
}
|
|
13417
|
-
function codingPrecisionAt(retrieved, relevant, k) {
|
|
13418
|
-
const rows = retrieved.slice(0, k);
|
|
13419
|
-
return rows.length ? rows.filter((item) => relevant.has(item.packet_id)).length / rows.length : 0;
|
|
13420
|
-
}
|
|
13421
|
-
function codingNdcgAt(retrieved, relevant, k) {
|
|
13422
|
-
const dcg = retrieved.slice(0, k).reduce((sum, item, index) => sum + (relevant.has(item.packet_id) ? 1 / Math.log2(index + 2) : 0), 0);
|
|
13423
|
-
const idealHits = Math.min(relevant.size, k);
|
|
13424
|
-
let ideal = 0;
|
|
13425
|
-
for (let index = 0; index < idealHits; index += 1)
|
|
13426
|
-
ideal += 1 / Math.log2(index + 2);
|
|
13427
|
-
return ideal ? dcg / ideal : 0;
|
|
13428
|
-
}
|
|
13429
|
-
function codingMrr(retrieved, relevant) {
|
|
13430
|
-
const index = retrieved.findIndex((item) => relevant.has(item.packet_id));
|
|
13431
|
-
return index >= 0 ? 1 / (index + 1) : 0;
|
|
13432
|
-
}
|
|
13433
13427
|
function codingTypeForCategory(category) {
|
|
13434
13428
|
if (category === "runbook")
|
|
13435
13429
|
return "runbook";
|
|
@@ -13442,31 +13436,6 @@ function codingTypeForCategory(category) {
|
|
|
13442
13436
|
function codingFileForTopic(topic, variant) {
|
|
13443
13437
|
return `src/${slugify(topic)}-${variant % 3}.ts`;
|
|
13444
13438
|
}
|
|
13445
|
-
function averageNumber(values) {
|
|
13446
|
-
return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
|
|
13447
|
-
}
|
|
13448
|
-
function percentileNumber(values, p) {
|
|
13449
|
-
if (!values.length)
|
|
13450
|
-
return 0;
|
|
13451
|
-
const sorted = values.slice().sort((a, b) => a - b);
|
|
13452
|
-
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * p) - 1));
|
|
13453
|
-
return sorted[index];
|
|
13454
|
-
}
|
|
13455
|
-
function roundDecimal(value, digits = 2) {
|
|
13456
|
-
const factor = 10 ** digits;
|
|
13457
|
-
return Math.round(value * factor) / factor;
|
|
13458
|
-
}
|
|
13459
|
-
function countByKey(rows, fn) {
|
|
13460
|
-
const counts = {};
|
|
13461
|
-
for (const row of rows) {
|
|
13462
|
-
const key = fn(row);
|
|
13463
|
-
counts[key] = (counts[key] ?? 0) + 1;
|
|
13464
|
-
}
|
|
13465
|
-
return counts;
|
|
13466
|
-
}
|
|
13467
|
-
function titleCase(value) {
|
|
13468
|
-
return value.replace(/\b[a-z]/g, (match) => match.toUpperCase());
|
|
13469
|
-
}
|
|
13470
13439
|
function baselineDiscoveryFiles(projectDir, task) {
|
|
13471
13440
|
const terms = tokenize(task);
|
|
13472
13441
|
const graph = buildCodeGraph(projectDir);
|
|
@@ -14750,6 +14719,36 @@ function verifyAgentActivation(agent, projectDir, options = {}) {
|
|
|
14750
14719
|
function observationPath(projectDir, id) {
|
|
14751
14720
|
return (0, node_path_1.join)(observationsDir(projectDir), `${id}.json`);
|
|
14752
14721
|
}
|
|
14722
|
+
// Observations are session-scoped raw signal: distill consumes them at session end and
|
|
14723
|
+
// the resume digest only reads the recent window. Without retention the directory grows
|
|
14724
|
+
// forever (measured: 12k files / 48MB in two months of dogfooding), so refresh prunes
|
|
14725
|
+
// records older than the retention window. 0 disables pruning.
|
|
14726
|
+
const OBSERVATION_RETENTION_DAYS = (() => {
|
|
14727
|
+
const raw = Number(process.env.KAGE_OBSERVATION_RETENTION_DAYS ?? "30");
|
|
14728
|
+
return Number.isFinite(raw) && raw >= 0 ? raw : 30;
|
|
14729
|
+
})();
|
|
14730
|
+
function pruneObservations(projectDir, maxAgeDays = OBSERVATION_RETENTION_DAYS) {
|
|
14731
|
+
if (maxAgeDays <= 0)
|
|
14732
|
+
return { pruned: 0 };
|
|
14733
|
+
const dir = observationsDir(projectDir);
|
|
14734
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
14735
|
+
return { pruned: 0 };
|
|
14736
|
+
const cutoff = Date.now() - maxAgeDays * 86_400_000;
|
|
14737
|
+
let pruned = 0;
|
|
14738
|
+
for (const name of (0, node_fs_1.readdirSync)(dir)) {
|
|
14739
|
+
if (!name.endsWith(".json"))
|
|
14740
|
+
continue;
|
|
14741
|
+
const path = (0, node_path_1.join)(dir, name);
|
|
14742
|
+
try {
|
|
14743
|
+
if ((0, node_fs_1.statSync)(path).mtimeMs < cutoff) {
|
|
14744
|
+
(0, node_fs_1.unlinkSync)(path);
|
|
14745
|
+
pruned += 1;
|
|
14746
|
+
}
|
|
14747
|
+
}
|
|
14748
|
+
catch { /* concurrent removal — skip */ }
|
|
14749
|
+
}
|
|
14750
|
+
return { pruned };
|
|
14751
|
+
}
|
|
14753
14752
|
function observationHash(projectDir, event) {
|
|
14754
14753
|
const bucket = event.timestamp ? new Date(event.timestamp).toISOString().slice(0, 16) : nowIso().slice(0, 16);
|
|
14755
14754
|
return (0, node_crypto_1.createHash)("sha256")
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Pure metric and statistics helpers used by the benchmark/metrics code.
|
|
3
|
+
// This is a dependency-free leaf module: it imports nothing from the kernel,
|
|
4
|
+
// so pulling it out carries no circular-dependency risk. Leaf-first is the
|
|
5
|
+
// safe way to decompose the kernel — extract the bottom of the dependency
|
|
6
|
+
// graph first, where nothing depends back inward.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.codingRecallAt = codingRecallAt;
|
|
9
|
+
exports.codingPrecisionAt = codingPrecisionAt;
|
|
10
|
+
exports.codingNdcgAt = codingNdcgAt;
|
|
11
|
+
exports.codingMrr = codingMrr;
|
|
12
|
+
exports.averageNumber = averageNumber;
|
|
13
|
+
exports.percentileNumber = percentileNumber;
|
|
14
|
+
exports.roundDecimal = roundDecimal;
|
|
15
|
+
exports.countByKey = countByKey;
|
|
16
|
+
exports.titleCase = titleCase;
|
|
17
|
+
/** Recall@k: fraction of the relevant set retrieved within the top k. */
|
|
18
|
+
function codingRecallAt(retrieved, relevant, k) {
|
|
19
|
+
if (!relevant.size)
|
|
20
|
+
return 0;
|
|
21
|
+
return retrieved.slice(0, k).filter((item) => relevant.has(item.packet_id)).length / relevant.size;
|
|
22
|
+
}
|
|
23
|
+
/** Precision@k: fraction of the top k that is relevant. */
|
|
24
|
+
function codingPrecisionAt(retrieved, relevant, k) {
|
|
25
|
+
const rows = retrieved.slice(0, k);
|
|
26
|
+
return rows.length ? rows.filter((item) => relevant.has(item.packet_id)).length / rows.length : 0;
|
|
27
|
+
}
|
|
28
|
+
/** Normalized discounted cumulative gain at k (binary relevance). */
|
|
29
|
+
function codingNdcgAt(retrieved, relevant, k) {
|
|
30
|
+
const dcg = retrieved.slice(0, k).reduce((sum, item, index) => sum + (relevant.has(item.packet_id) ? 1 / Math.log2(index + 2) : 0), 0);
|
|
31
|
+
const idealHits = Math.min(relevant.size, k);
|
|
32
|
+
let ideal = 0;
|
|
33
|
+
for (let index = 0; index < idealHits; index += 1)
|
|
34
|
+
ideal += 1 / Math.log2(index + 2);
|
|
35
|
+
return ideal ? dcg / ideal : 0;
|
|
36
|
+
}
|
|
37
|
+
/** Mean reciprocal rank: 1 / rank of the first relevant hit, else 0. */
|
|
38
|
+
function codingMrr(retrieved, relevant) {
|
|
39
|
+
const index = retrieved.findIndex((item) => relevant.has(item.packet_id));
|
|
40
|
+
return index >= 0 ? 1 / (index + 1) : 0;
|
|
41
|
+
}
|
|
42
|
+
/** Arithmetic mean, 0 for an empty list. */
|
|
43
|
+
function averageNumber(values) {
|
|
44
|
+
return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
|
|
45
|
+
}
|
|
46
|
+
/** p-th percentile (0..1) via nearest-rank, 0 for an empty list. */
|
|
47
|
+
function percentileNumber(values, p) {
|
|
48
|
+
if (!values.length)
|
|
49
|
+
return 0;
|
|
50
|
+
const sorted = values.slice().sort((a, b) => a - b);
|
|
51
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * p) - 1));
|
|
52
|
+
return sorted[index];
|
|
53
|
+
}
|
|
54
|
+
/** Round to a fixed number of decimal places. */
|
|
55
|
+
function roundDecimal(value, digits = 2) {
|
|
56
|
+
const factor = 10 ** digits;
|
|
57
|
+
return Math.round(value * factor) / factor;
|
|
58
|
+
}
|
|
59
|
+
/** Tally rows by a string key. */
|
|
60
|
+
function countByKey(rows, fn) {
|
|
61
|
+
const counts = {};
|
|
62
|
+
for (const row of rows) {
|
|
63
|
+
const key = fn(row);
|
|
64
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
65
|
+
}
|
|
66
|
+
return counts;
|
|
67
|
+
}
|
|
68
|
+
/** Upper-case the first letter of each word. */
|
|
69
|
+
function titleCase(value) {
|
|
70
|
+
return value.replace(/\b[a-z]/g, (match) => match.toUpperCase());
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kage-core/kage-graph-mcp",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"description": "Agent memory on Google's Open Knowledge Format (OKF). Kage maintains your coding agents' OKF memory bundle in git and verifies every concept against your code, deterministically. The verification and freshness layer OKF leaves out. MCP server, no account, no API key.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist/**/*.js",
|
package/viewer/console.js
CHANGED
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
// ---- nav ----
|
|
68
68
|
var META = {
|
|
69
69
|
dashboard: ["kage://dashboard", "Dashboard", "Your team's captured knowledge — decisions, runbooks, and fixes — at a glance."],
|
|
70
|
-
gains: ["kage://gains", "What Kage saved you", "Tokens
|
|
70
|
+
gains: ["kage://gains", "What Kage saved you", "Tokens saved and bad memories caught — receipts, not vibes."],
|
|
71
71
|
overview: ["kage://trust", "Memory trust", "Whether this repo's agent memory can be trusted — at a glance."],
|
|
72
72
|
graph: ["kage://memory-map", "Memory ↔ code map", "Each packet anchored to the files it's grounded in. Hover a node to inspect."],
|
|
73
73
|
memory: ["kage://memory", "Memory", "Every packet Kage has stored, with health and grounding."],
|
|
@@ -137,12 +137,6 @@
|
|
|
137
137
|
// value.json is the raw ledger written by recall: { totals, events[] }. Windows are
|
|
138
138
|
// recomputed here with the same rules as `kage gains` (today = local midnight,
|
|
139
139
|
// 7d = rolling, all-time = totals so trimmed events never lose history).
|
|
140
|
-
var DOLLARS_PER_MILLION_TOKENS = 15;
|
|
141
|
-
function dollars(tokens) { return (tokens / 1e6) * DOLLARS_PER_MILLION_TOKENS; }
|
|
142
|
-
function fmtDollars(tokens) {
|
|
143
|
-
var d = dollars(tokens);
|
|
144
|
-
return "$" + (d >= 100 ? Math.round(d) : d.toFixed(2));
|
|
145
|
-
}
|
|
146
140
|
function summarizeWindow(events, cutoff) {
|
|
147
141
|
var w = { tokens_saved: 0, stale_withheld: 0, recalls: 0, caller_answers: 0 };
|
|
148
142
|
events.forEach(function (e) {
|
|
@@ -175,7 +169,6 @@
|
|
|
175
169
|
var big = el("div", "big");
|
|
176
170
|
big.appendChild(el("b", null, all.tokens_saved ? "0" : "—"));
|
|
177
171
|
big.appendChild(el("span", "unit", "tokens saved, all time"));
|
|
178
|
-
if (all.tokens_saved) big.appendChild(el("span", "dollars", "≈ " + fmtDollars(all.tokens_saved)));
|
|
179
172
|
hh.appendChild(big);
|
|
180
173
|
hh.appendChild(el("p", "sub", all.tokens_saved
|
|
181
174
|
? "Context your agents did not have to re-read from source, because Kage served grounded memory instead."
|
|
@@ -186,7 +179,6 @@
|
|
|
186
179
|
var w = el("div", "r-win");
|
|
187
180
|
w.appendChild(el("div", "k", p[0]));
|
|
188
181
|
w.appendChild(el("div", "v", fmt(p[1].tokens_saved) + " tok"));
|
|
189
|
-
w.appendChild(el("div", "d", "≈ " + fmtDollars(p[1].tokens_saved)));
|
|
190
182
|
wins.appendChild(w);
|
|
191
183
|
});
|
|
192
184
|
hero.appendChild(wins);
|
|
@@ -203,13 +195,13 @@
|
|
|
203
195
|
lines.appendChild(row);
|
|
204
196
|
});
|
|
205
197
|
hero.appendChild(lines);
|
|
206
|
-
hero.appendChild(el("div", "r-foot", "
|
|
198
|
+
hero.appendChild(el("div", "r-foot", "ledger: .agent_memory/reports/value.json · verify: kage gains"));
|
|
207
199
|
if (all.tokens_saved) countUp(big.querySelector("b"), all.tokens_saved, 900);
|
|
208
200
|
|
|
209
201
|
if (tiles) {
|
|
210
202
|
tiles.textContent = "";
|
|
211
203
|
[
|
|
212
|
-
{ k: "Saved (7 days)", v: fmt(week.tokens_saved), s: "
|
|
204
|
+
{ k: "Saved (7 days)", v: fmt(week.tokens_saved), s: "tokens of context not re-read", cls: "green" },
|
|
213
205
|
{ k: "Recalls served (7d)", v: fmt(week.recalls), s: fmt(all.recalls) + " all-time", cls: "green" },
|
|
214
206
|
{ k: "Stale caught", v: fmt(all.stale_withheld), s: all.stale_withheld ? "withheld before they misled an agent" : "nothing withheld yet", cls: all.stale_withheld ? "warn" : "" },
|
|
215
207
|
{ k: "Graph answers", v: fmt(all.caller_answers), s: "caller questions answered from the code graph", cls: "code" },
|
package/viewer/index.html
CHANGED
|
@@ -69,14 +69,12 @@
|
|
|
69
69
|
.receipt .r-hero .big { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
|
|
70
70
|
.receipt .r-hero .big b { font: 600 64px/0.95 var(--serif); letter-spacing: -0.02em; color: var(--gain); font-variant-numeric: tabular-nums; }
|
|
71
71
|
.receipt .r-hero .big .unit { color: var(--muted); font: 500 16px/1 var(--sans); }
|
|
72
|
-
.receipt .r-hero .big .dollars { color: var(--ink); font: 600 22px/1 var(--serif); }
|
|
73
72
|
.receipt .r-hero .sub { margin-top: 10px; color: var(--muted); font: 400 13.5px/1.5 var(--sans); max-width: 560px; }
|
|
74
73
|
.receipt .r-windows { display: grid; grid-template-columns: repeat(3, 1fr); border-top: 1px dashed var(--line-strong); }
|
|
75
74
|
.receipt .r-win { padding: 16px 24px; border-left: 1px dashed var(--line-strong); }
|
|
76
75
|
.receipt .r-win:first-child { border-left: 0; }
|
|
77
76
|
.receipt .r-win .k { color: var(--faint); font: 600 10px/1 var(--mono); text-transform: uppercase; letter-spacing: 0.1em; }
|
|
78
77
|
.receipt .r-win .v { margin-top: 8px; font: 600 24px/1 var(--serif); color: var(--ink); font-variant-numeric: tabular-nums; }
|
|
79
|
-
.receipt .r-win .d { margin-top: 5px; color: var(--gain); font: 600 12px/1 var(--mono); }
|
|
80
78
|
.receipt .r-lines { border-top: 1px dashed var(--line-strong); padding: 14px 24px 16px; }
|
|
81
79
|
.receipt .r-line { display: flex; align-items: baseline; gap: 10px; padding: 5px 0; font: 400 13.5px/1.5 var(--sans); color: var(--ink); }
|
|
82
80
|
.receipt .r-line .dots { flex: 1; border-bottom: 1px dotted var(--line-strong); transform: translateY(-4px); }
|
|
@@ -290,7 +288,7 @@
|
|
|
290
288
|
<header class="head">
|
|
291
289
|
<span class="eyebrow" id="eyebrow">kage://gains</span>
|
|
292
290
|
<h1 id="title">What Kage saved you</h1>
|
|
293
|
-
<p id="subtitle">Tokens
|
|
291
|
+
<p id="subtitle">Tokens saved and bad memories caught — receipts, not vibes.</p>
|
|
294
292
|
</header>
|
|
295
293
|
<div class="wrap">
|
|
296
294
|
<section class="section active" id="section-dashboard">
|