@gethmy/mcp 2.8.4 → 2.8.6
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/cli.js +438 -39
- package/dist/index.js +438 -39
- package/dist/lib/api-client.js +18 -4
- package/package.json +1 -1
- package/src/api-client.ts +6 -0
- package/src/auto-session.ts +18 -9
- package/src/graph-expansion.ts +151 -0
- package/src/memory-park.ts +183 -4
- package/src/memory-provenance.ts +177 -0
- package/src/memory-tags.ts +88 -0
- package/src/server.ts +336 -53
package/dist/cli.js
CHANGED
|
@@ -1123,10 +1123,14 @@ function getDisplayLinkType(linkType, direction) {
|
|
|
1123
1123
|
}
|
|
1124
1124
|
// ../harmony-shared/dist/commentSerializer.js
|
|
1125
1125
|
var CONFLICT_INSTRUCTION = "When two comments conflict, prefer the latest created_at, UNLESS a later " + "comment explicitly confirms or restates the earlier finding. Evaluate " + "substance, not just recency. Cite the comment id(s) you relied on.";
|
|
1126
|
+
function sanitizeHeaderField(value) {
|
|
1127
|
+
return value.replace(/[\]\r\n|<>]/g, " ").trim() || "—";
|
|
1128
|
+
}
|
|
1126
1129
|
function authorLabel(c) {
|
|
1127
1130
|
if (c.author_type === "agent")
|
|
1128
1131
|
return "AI agent";
|
|
1129
|
-
|
|
1132
|
+
const raw = c.author?.full_name || "teammate";
|
|
1133
|
+
return sanitizeHeaderField(raw);
|
|
1130
1134
|
}
|
|
1131
1135
|
function criticalIds(comments) {
|
|
1132
1136
|
const keep = new Set;
|
|
@@ -1183,9 +1187,15 @@ function serializeCommentThread(comments, options = {}) {
|
|
|
1183
1187
|
if (c.resolved_at)
|
|
1184
1188
|
tags.push("resolved");
|
|
1185
1189
|
const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
|
|
1186
|
-
const header = `[${ref(c.id)} | ${c.author_type} | ${authorLabel(c)} | ${c.comment_type} | ${c.created_at}${tagStr}]`;
|
|
1187
|
-
|
|
1188
|
-
|
|
1190
|
+
const header = `[${sanitizeHeaderField(ref(c.id))} | ${sanitizeHeaderField(c.author_type)} | ${authorLabel(c)} | ${sanitizeHeaderField(c.comment_type)} | ${sanitizeHeaderField(c.created_at)}${tagStr}]`;
|
|
1191
|
+
const fencedBody = c.body.trim().replaceAll("<", "<").replaceAll(">", ">");
|
|
1192
|
+
lines.push({
|
|
1193
|
+
at: c.created_at,
|
|
1194
|
+
text: `${header}
|
|
1195
|
+
<comment-body>
|
|
1196
|
+
${fencedBody}
|
|
1197
|
+
</comment-body>`
|
|
1198
|
+
});
|
|
1189
1199
|
}
|
|
1190
1200
|
for (const a of activity) {
|
|
1191
1201
|
const actor = a.actor ? `${a.actor} ` : "";
|
|
@@ -1710,6 +1720,10 @@ class HarmonyApiClient {
|
|
|
1710
1720
|
params.set("type", options.type);
|
|
1711
1721
|
if (options?.limit !== undefined)
|
|
1712
1722
|
params.set("limit", String(options.limit));
|
|
1723
|
+
for (const tag of options?.tags ?? [])
|
|
1724
|
+
params.append("tags", tag);
|
|
1725
|
+
if (options?.include_superseded)
|
|
1726
|
+
params.set("include_superseded", "true");
|
|
1713
1727
|
return this.request("GET", `/memory/search?${params.toString()}`);
|
|
1714
1728
|
}
|
|
1715
1729
|
async getVaultIndex(options) {
|
|
@@ -2027,7 +2041,6 @@ function resolveAgentIdentity(info) {
|
|
|
2027
2041
|
}
|
|
2028
2042
|
var AUTO_START_TRIGGERS = new Set([
|
|
2029
2043
|
"harmony_generate_prompt",
|
|
2030
|
-
"harmony_update_card",
|
|
2031
2044
|
"harmony_create_subtask",
|
|
2032
2045
|
"harmony_toggle_subtask",
|
|
2033
2046
|
"harmony_update_subtask"
|
|
@@ -2059,6 +2072,10 @@ async function trackActivity(cardId, options) {
|
|
|
2059
2072
|
const client3 = options?.client ?? clientGetter?.();
|
|
2060
2073
|
if (!client3)
|
|
2061
2074
|
return;
|
|
2075
|
+
const info = clientInfoGetter?.() ?? null;
|
|
2076
|
+
if (!info?.name)
|
|
2077
|
+
return;
|
|
2078
|
+
const { agentIdentifier, agentName } = resolveAgentIdentity(info);
|
|
2062
2079
|
const toEnd = [];
|
|
2063
2080
|
for (const [otherCardId, session] of activeSessions) {
|
|
2064
2081
|
if (otherCardId !== cardId && !session.isExplicit) {
|
|
@@ -2068,8 +2085,6 @@ async function trackActivity(cardId, options) {
|
|
|
2068
2085
|
for (const otherCardId of toEnd) {
|
|
2069
2086
|
await autoEndSession(client3, otherCardId, "completed");
|
|
2070
2087
|
}
|
|
2071
|
-
const info = clientInfoGetter?.() ?? null;
|
|
2072
|
-
const { agentIdentifier, agentName } = resolveAgentIdentity(info);
|
|
2073
2088
|
try {
|
|
2074
2089
|
await client3.startAgentSession(cardId, {
|
|
2075
2090
|
agentIdentifier,
|
|
@@ -2189,6 +2204,90 @@ async function autoExpandGraph(client3, entityId, title, content, _tags, workspa
|
|
|
2189
2204
|
return { relationsCreated: 0 };
|
|
2190
2205
|
}
|
|
2191
2206
|
}
|
|
2207
|
+
async function findSimilarEntities(client3, title, content, workspaceId, options) {
|
|
2208
|
+
const contentSnippet = content.slice(0, 200).trim();
|
|
2209
|
+
const query = [title, contentSnippet].filter(Boolean).join(" ");
|
|
2210
|
+
try {
|
|
2211
|
+
const { entities } = await client3.searchMemoryEntities(workspaceId, query, {
|
|
2212
|
+
project_id: options?.projectId,
|
|
2213
|
+
limit: options?.limit ?? 20,
|
|
2214
|
+
type: options?.type
|
|
2215
|
+
});
|
|
2216
|
+
const minScore = options?.minRrfScore ?? 0;
|
|
2217
|
+
const excludeSet = new Set(options?.excludeIds || []);
|
|
2218
|
+
return entities.filter((e) => {
|
|
2219
|
+
if (excludeSet.has(e.id))
|
|
2220
|
+
return false;
|
|
2221
|
+
if (minScore > 0 && (e.rrf_score ?? 0) < minScore)
|
|
2222
|
+
return false;
|
|
2223
|
+
return true;
|
|
2224
|
+
});
|
|
2225
|
+
} catch {
|
|
2226
|
+
return [];
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
var SUPERSEDE_RRF_THRESHOLD = 0.029;
|
|
2230
|
+
var SUPERSEDE_TITLE_OVERLAP = 0.5;
|
|
2231
|
+
var TITLE_STOPWORDS = new Set([
|
|
2232
|
+
"a",
|
|
2233
|
+
"an",
|
|
2234
|
+
"the",
|
|
2235
|
+
"and",
|
|
2236
|
+
"or",
|
|
2237
|
+
"of",
|
|
2238
|
+
"to",
|
|
2239
|
+
"in",
|
|
2240
|
+
"on",
|
|
2241
|
+
"for",
|
|
2242
|
+
"with",
|
|
2243
|
+
"is",
|
|
2244
|
+
"are",
|
|
2245
|
+
"be",
|
|
2246
|
+
"by",
|
|
2247
|
+
"at",
|
|
2248
|
+
"as"
|
|
2249
|
+
]);
|
|
2250
|
+
function significantTitleTokens(title) {
|
|
2251
|
+
return new Set(title.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 2 && !TITLE_STOPWORDS.has(t)));
|
|
2252
|
+
}
|
|
2253
|
+
function jaccard(a, b) {
|
|
2254
|
+
if (a.size === 0 || b.size === 0)
|
|
2255
|
+
return 0;
|
|
2256
|
+
let intersection = 0;
|
|
2257
|
+
for (const t of a)
|
|
2258
|
+
if (b.has(t))
|
|
2259
|
+
intersection++;
|
|
2260
|
+
return intersection / (a.size + b.size - intersection);
|
|
2261
|
+
}
|
|
2262
|
+
async function findSupersedeCandidates(client3, title, content, type, workspaceId, options) {
|
|
2263
|
+
const rrfThreshold = options?.rrfThreshold ?? SUPERSEDE_RRF_THRESHOLD;
|
|
2264
|
+
const titleOverlap = options?.titleOverlap ?? SUPERSEDE_TITLE_OVERLAP;
|
|
2265
|
+
const candidateTokens = significantTitleTokens(title);
|
|
2266
|
+
try {
|
|
2267
|
+
const hits = await findSimilarEntities(client3, title, content, workspaceId, {
|
|
2268
|
+
projectId: options?.projectId,
|
|
2269
|
+
type,
|
|
2270
|
+
limit: options?.limit ?? 10,
|
|
2271
|
+
minRrfScore: rrfThreshold
|
|
2272
|
+
});
|
|
2273
|
+
return hits.filter((e) => {
|
|
2274
|
+
if (options?.scope && e.scope !== undefined) {
|
|
2275
|
+
if (e.scope !== options.scope)
|
|
2276
|
+
return false;
|
|
2277
|
+
}
|
|
2278
|
+
if (e.superseded_at) {
|
|
2279
|
+
return false;
|
|
2280
|
+
}
|
|
2281
|
+
return jaccard(candidateTokens, significantTitleTokens(e.title)) >= titleOverlap;
|
|
2282
|
+
}).map((e) => ({
|
|
2283
|
+
id: e.id,
|
|
2284
|
+
title: e.title,
|
|
2285
|
+
score: e.rrf_score ?? 0
|
|
2286
|
+
}));
|
|
2287
|
+
} catch {
|
|
2288
|
+
return [];
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2192
2291
|
|
|
2193
2292
|
// src/memory-floor.ts
|
|
2194
2293
|
var STOP_WORDS = new Set([
|
|
@@ -2327,6 +2426,10 @@ var DEFAULT_WEIGHTS = {
|
|
|
2327
2426
|
recency: 0.25,
|
|
2328
2427
|
importance: 0.2
|
|
2329
2428
|
};
|
|
2429
|
+
var USAGE_BUMP_SCALE = 0.6;
|
|
2430
|
+
var USAGE_BUMP_MAX = 2;
|
|
2431
|
+
var FEEDBACK_BUMP_SCALE = 0.8;
|
|
2432
|
+
var FEEDBACK_BUMP_MAX = 2;
|
|
2330
2433
|
var TYPE_TAU_SECONDS = {
|
|
2331
2434
|
preference: Number.POSITIVE_INFINITY,
|
|
2332
2435
|
pattern: 60 * 60 * 24 * 180,
|
|
@@ -2382,8 +2485,37 @@ function recencyDecay(lastAccessedAt, createdAt, type, now) {
|
|
|
2382
2485
|
const dtSec = Math.max(0, (now.getTime() - t) / 1000);
|
|
2383
2486
|
return clamp01(Math.exp(-dtSec / tau));
|
|
2384
2487
|
}
|
|
2385
|
-
function
|
|
2488
|
+
function baseImportance(raw, type) {
|
|
2386
2489
|
let v = typeof raw === "number" ? raw : TYPE_IMPORTANCE_DEFAULT[type] ?? 5;
|
|
2490
|
+
if (v < 1)
|
|
2491
|
+
v = 1;
|
|
2492
|
+
if (v > 10)
|
|
2493
|
+
v = 10;
|
|
2494
|
+
return v;
|
|
2495
|
+
}
|
|
2496
|
+
function usageBump(accessCount) {
|
|
2497
|
+
const n = typeof accessCount === "number" && accessCount > 0 ? accessCount : 0;
|
|
2498
|
+
if (n === 0)
|
|
2499
|
+
return 0;
|
|
2500
|
+
return Math.min(USAGE_BUMP_MAX, USAGE_BUMP_SCALE * Math.log(1 + n));
|
|
2501
|
+
}
|
|
2502
|
+
function feedbackBump(feedback) {
|
|
2503
|
+
const up = typeof feedback?.up === "number" ? feedback.up : 0;
|
|
2504
|
+
const down = typeof feedback?.down === "number" ? feedback.down : 0;
|
|
2505
|
+
const net = up - down;
|
|
2506
|
+
if (net === 0)
|
|
2507
|
+
return 0;
|
|
2508
|
+
const raw = FEEDBACK_BUMP_SCALE * Math.sign(net) * Math.log(1 + Math.abs(net));
|
|
2509
|
+
if (raw > FEEDBACK_BUMP_MAX)
|
|
2510
|
+
return FEEDBACK_BUMP_MAX;
|
|
2511
|
+
if (raw < -FEEDBACK_BUMP_MAX)
|
|
2512
|
+
return -FEEDBACK_BUMP_MAX;
|
|
2513
|
+
return raw;
|
|
2514
|
+
}
|
|
2515
|
+
function effectiveImportance(entity) {
|
|
2516
|
+
const base = baseImportance(entity.importance, entity.type);
|
|
2517
|
+
const bump = usageBump(entity.access_count) + feedbackBump(entity.metadata?.feedback);
|
|
2518
|
+
let v = base + bump;
|
|
2387
2519
|
if (v < 1)
|
|
2388
2520
|
v = 1;
|
|
2389
2521
|
if (v > 10)
|
|
@@ -2397,7 +2529,7 @@ function rescore(entities, options = {}) {
|
|
|
2397
2529
|
const scored = entities.map((entity) => {
|
|
2398
2530
|
const relevance = clamp01(relevanceMap.get(entity.id ?? "") ?? 0.5);
|
|
2399
2531
|
const recency = recencyDecay(entity.last_accessed_at, entity.created_at, entity.type, now);
|
|
2400
|
-
const importance =
|
|
2532
|
+
const importance = effectiveImportance(entity);
|
|
2401
2533
|
const score = w.relevance * relevance + w.recency * recency + w.importance * importance;
|
|
2402
2534
|
return { entity, relevance, recency, importance, score };
|
|
2403
2535
|
});
|
|
@@ -2410,6 +2542,11 @@ function rescore(entities, options = {}) {
|
|
|
2410
2542
|
});
|
|
2411
2543
|
return scored;
|
|
2412
2544
|
}
|
|
2545
|
+
function filterByMinConfidence(entities, minConfidence) {
|
|
2546
|
+
if (typeof minConfidence !== "number")
|
|
2547
|
+
return entities;
|
|
2548
|
+
return entities.filter((e) => typeof e.confidence === "number" && e.confidence >= minConfidence);
|
|
2549
|
+
}
|
|
2413
2550
|
function relevanceFromRank(ranked, decay = 10) {
|
|
2414
2551
|
const out = new Map;
|
|
2415
2552
|
ranked.forEach((entity, rank) => {
|
|
@@ -2442,6 +2579,82 @@ function fitToBudget(scored, budgetTokens) {
|
|
|
2442
2579
|
}
|
|
2443
2580
|
return out;
|
|
2444
2581
|
}
|
|
2582
|
+
function mergeFeedback(existing, vote) {
|
|
2583
|
+
const up = typeof existing?.up === "number" && existing.up > 0 ? existing.up : 0;
|
|
2584
|
+
const down = typeof existing?.down === "number" && existing.down > 0 ? existing.down : 0;
|
|
2585
|
+
return vote === "up" ? { up: up + 1, down } : { up, down: down + 1 };
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
// src/memory-provenance.ts
|
|
2589
|
+
function buildOrigin(input) {
|
|
2590
|
+
const origin = { source: input.source };
|
|
2591
|
+
if (input.source_card_id)
|
|
2592
|
+
origin.source_card_id = input.source_card_id;
|
|
2593
|
+
if (input.source_session_id)
|
|
2594
|
+
origin.source_session_id = input.source_session_id;
|
|
2595
|
+
if (input.author)
|
|
2596
|
+
origin.author = input.author;
|
|
2597
|
+
if (input.source_trust)
|
|
2598
|
+
origin.source_trust = input.source_trust;
|
|
2599
|
+
return origin;
|
|
2600
|
+
}
|
|
2601
|
+
function defaultConfidenceForSource(input) {
|
|
2602
|
+
if (input.source_trust === "document")
|
|
2603
|
+
return 0.9;
|
|
2604
|
+
if (input.source_trust === "manual")
|
|
2605
|
+
return 0.9;
|
|
2606
|
+
switch (input.source) {
|
|
2607
|
+
case "manual":
|
|
2608
|
+
return 0.9;
|
|
2609
|
+
case "assistant":
|
|
2610
|
+
return 0.8;
|
|
2611
|
+
case "agent-run":
|
|
2612
|
+
return 0.6;
|
|
2613
|
+
case "import":
|
|
2614
|
+
return 0.6;
|
|
2615
|
+
default:
|
|
2616
|
+
return 0.7;
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
var TYPE_IMPORTANCE_DEFAULT2 = {
|
|
2620
|
+
preference: 9,
|
|
2621
|
+
lesson: 8,
|
|
2622
|
+
decision: 8,
|
|
2623
|
+
pattern: 7,
|
|
2624
|
+
solution: 7,
|
|
2625
|
+
procedure: 7,
|
|
2626
|
+
error: 5,
|
|
2627
|
+
context: 5,
|
|
2628
|
+
task: 5,
|
|
2629
|
+
agent: 5,
|
|
2630
|
+
relationship: 6,
|
|
2631
|
+
commitment: 7,
|
|
2632
|
+
project: 6,
|
|
2633
|
+
handoff: 6
|
|
2634
|
+
};
|
|
2635
|
+
var FILE_PATH = /(?:[\w.-]+\/){1,}[\w.-]+\.[a-z]{1,4}\b|(?:[\w-]+\/){2,}[\w-]+/i;
|
|
2636
|
+
var GUIDANCE_SECTION = /\b(why|how to apply|how to use|takeaway|root cause)\b/i;
|
|
2637
|
+
var PROPER_NOUN = /`[^`]+`|\b[A-Z][a-z0-9]+[A-Z][A-Za-z0-9]*\b|\b[a-z]+_[a-z][\w]*\b/;
|
|
2638
|
+
function signalDensity(content) {
|
|
2639
|
+
let n = 0;
|
|
2640
|
+
if (FILE_PATH.test(content))
|
|
2641
|
+
n += 1;
|
|
2642
|
+
if (GUIDANCE_SECTION.test(content))
|
|
2643
|
+
n += 1;
|
|
2644
|
+
if (PROPER_NOUN.test(content))
|
|
2645
|
+
n += 1;
|
|
2646
|
+
return n;
|
|
2647
|
+
}
|
|
2648
|
+
function importanceWithSignalBump(type, content) {
|
|
2649
|
+
const base = TYPE_IMPORTANCE_DEFAULT2[type] ?? 5;
|
|
2650
|
+
const bump = Math.min(2, signalDensity(content));
|
|
2651
|
+
const v = base + bump;
|
|
2652
|
+
if (v < 1)
|
|
2653
|
+
return 1;
|
|
2654
|
+
if (v > 10)
|
|
2655
|
+
return 10;
|
|
2656
|
+
return v;
|
|
2657
|
+
}
|
|
2445
2658
|
|
|
2446
2659
|
// src/memory-session.ts
|
|
2447
2660
|
var SESSION_SCOPE_PREFIX = "session:";
|
|
@@ -2462,6 +2675,49 @@ function sessionScopeFor(agentSessionId) {
|
|
|
2462
2675
|
return `${SESSION_SCOPE_PREFIX}${agentSessionId}`;
|
|
2463
2676
|
}
|
|
2464
2677
|
|
|
2678
|
+
// src/memory-tags.ts
|
|
2679
|
+
var CARD_REF = /^card[\s:#-]+(\d+)$/;
|
|
2680
|
+
function normalizeTag(tag) {
|
|
2681
|
+
const cleaned = tag.trim().toLowerCase().replace(/\s+/g, " ");
|
|
2682
|
+
const cardMatch = cleaned.match(CARD_REF);
|
|
2683
|
+
if (cardMatch) {
|
|
2684
|
+
return `card:${cardMatch[1]}`;
|
|
2685
|
+
}
|
|
2686
|
+
return cleaned;
|
|
2687
|
+
}
|
|
2688
|
+
function normalizeTags(tags) {
|
|
2689
|
+
const seen = new Set;
|
|
2690
|
+
const out = [];
|
|
2691
|
+
for (const raw of tags) {
|
|
2692
|
+
const norm = normalizeTag(raw);
|
|
2693
|
+
if (!norm || seen.has(norm))
|
|
2694
|
+
continue;
|
|
2695
|
+
seen.add(norm);
|
|
2696
|
+
out.push(norm);
|
|
2697
|
+
}
|
|
2698
|
+
return out;
|
|
2699
|
+
}
|
|
2700
|
+
function lintTags(tags) {
|
|
2701
|
+
const suggestions = [];
|
|
2702
|
+
for (const tag of tags) {
|
|
2703
|
+
const norm = normalizeTag(tag);
|
|
2704
|
+
if (norm === tag)
|
|
2705
|
+
continue;
|
|
2706
|
+
let reason;
|
|
2707
|
+
if (CARD_REF.test(tag.trim().toLowerCase()) && norm.startsWith("card:")) {
|
|
2708
|
+
reason = "non-canonical card ref; use 'card:<n>'";
|
|
2709
|
+
} else if (tag !== tag.trim() || /\s{2,}/.test(tag)) {
|
|
2710
|
+
reason = "whitespace; trim and collapse";
|
|
2711
|
+
} else if (tag !== tag.toLowerCase()) {
|
|
2712
|
+
reason = "case variant; use lowercase";
|
|
2713
|
+
} else {
|
|
2714
|
+
reason = "normalize to canonical form";
|
|
2715
|
+
}
|
|
2716
|
+
suggestions.push({ tag, suggestion: norm, reason });
|
|
2717
|
+
}
|
|
2718
|
+
return suggestions;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2465
2721
|
// src/onboard.ts
|
|
2466
2722
|
async function onboardNewUser(params) {
|
|
2467
2723
|
const {
|
|
@@ -3679,6 +3935,10 @@ var TOOLS = {
|
|
|
3679
3935
|
projectId: {
|
|
3680
3936
|
type: "string",
|
|
3681
3937
|
description: "Project ID (optional, required if scope is 'project')"
|
|
3938
|
+
},
|
|
3939
|
+
supersedeId: {
|
|
3940
|
+
type: "string",
|
|
3941
|
+
description: "Optional. ID of an existing memory entity to soft-supersede with " + "this new one (sets its superseded_by/superseded_at). Use after a " + "previous harmony_remember returned a `similar` candidate you want " + "to replace. Reversible; never deletes or merges content. The new " + "entity is always created regardless."
|
|
3682
3942
|
}
|
|
3683
3943
|
},
|
|
3684
3944
|
required: ["title", "content"]
|
|
@@ -3852,6 +4112,49 @@ var TOOLS = {
|
|
|
3852
4112
|
required: ["sourceId", "targetId", "relationType"]
|
|
3853
4113
|
}
|
|
3854
4114
|
},
|
|
4115
|
+
harmony_suggest_relations: {
|
|
4116
|
+
description: "Suggest existing memory entities that look related to a given entity, using the same hybrid vector+FTS similarity as recall (card #281). Read-only: it NEVER creates relations — it only proposes candidates for a caller (human/agent) to relate explicitly via harmony_relate. Use to surface 'these look related — link?' candidates that would activate graph-walk retrieval.",
|
|
4117
|
+
inputSchema: {
|
|
4118
|
+
type: "object",
|
|
4119
|
+
properties: {
|
|
4120
|
+
entityId: {
|
|
4121
|
+
type: "string",
|
|
4122
|
+
description: "The entity UUID to find related candidates for"
|
|
4123
|
+
},
|
|
4124
|
+
limit: {
|
|
4125
|
+
type: "number",
|
|
4126
|
+
description: "Max suggestions to return (default: 5)"
|
|
4127
|
+
},
|
|
4128
|
+
workspaceId: {
|
|
4129
|
+
type: "string",
|
|
4130
|
+
description: "Workspace ID (optional if context set)"
|
|
4131
|
+
},
|
|
4132
|
+
projectId: {
|
|
4133
|
+
type: "string",
|
|
4134
|
+
description: "Project ID (optional)"
|
|
4135
|
+
}
|
|
4136
|
+
},
|
|
4137
|
+
required: ["entityId"]
|
|
4138
|
+
}
|
|
4139
|
+
},
|
|
4140
|
+
harmony_recall_feedback: {
|
|
4141
|
+
description: "Record a \uD83D\uDC4D/\uD83D\uDC4E on a recalled memory so future recalls can rank it better or worse. Non-destructive: stores a counter at metadata.feedback={up,down} and only affects ranking — it never deletes, hides, or supersedes the memory. Call after a recall when a memory proved useful (vote 'up') or misleading/irrelevant (vote 'down').",
|
|
4142
|
+
inputSchema: {
|
|
4143
|
+
type: "object",
|
|
4144
|
+
properties: {
|
|
4145
|
+
entityId: {
|
|
4146
|
+
type: "string",
|
|
4147
|
+
description: "Memory entity UUID that was recalled"
|
|
4148
|
+
},
|
|
4149
|
+
vote: {
|
|
4150
|
+
type: "string",
|
|
4151
|
+
enum: ["up", "down"],
|
|
4152
|
+
description: "'up' if the memory was useful, 'down' if it was misleading or irrelevant"
|
|
4153
|
+
}
|
|
4154
|
+
},
|
|
4155
|
+
required: ["entityId", "vote"]
|
|
4156
|
+
}
|
|
4157
|
+
},
|
|
3855
4158
|
harmony_memory_search: {
|
|
3856
4159
|
description: "Full-text and semantic search across the knowledge base. Uses hybrid vector+FTS search (when embeddings are available) for best results. Returns entities ranked by relevance.",
|
|
3857
4160
|
inputSchema: {
|
|
@@ -4929,7 +5232,9 @@ async function handleToolCall(name, args, deps) {
|
|
|
4929
5232
|
throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
|
|
4930
5233
|
}
|
|
4931
5234
|
const entityType = args.type || "context";
|
|
4932
|
-
const
|
|
5235
|
+
const rawTags = args.tags || [];
|
|
5236
|
+
const entityTags = normalizeTags(rawTags);
|
|
5237
|
+
const tagSuggestions = lintTags(rawTags);
|
|
4933
5238
|
const activeMemSession = getActiveMemorySession();
|
|
4934
5239
|
const entityScope = resolveSessionScope(args.scope, activeMemSession?.agentSessionId) || "project";
|
|
4935
5240
|
const floorRejection = validateMemoryQuality({
|
|
@@ -4943,24 +5248,59 @@ async function handleToolCall(name, args, deps) {
|
|
|
4943
5248
|
if (floorRejection) {
|
|
4944
5249
|
throw new Error(`Memory rejected by Utility Floor [${floorRejection.rule}]: ${floorRejection.message}`);
|
|
4945
5250
|
}
|
|
4946
|
-
const
|
|
5251
|
+
const sourceTrust = args.source_trust;
|
|
5252
|
+
const source = activeMemSession ? activeMemSession.agentIdentifier === "assistant" ? "assistant" : "agent-run" : "manual";
|
|
5253
|
+
const origin = buildOrigin({
|
|
5254
|
+
source,
|
|
5255
|
+
source_card_id: activeMemSession?.cardId,
|
|
5256
|
+
source_session_id: activeMemSession?.agentSessionId,
|
|
5257
|
+
author: activeMemSession?.agentIdentifier ?? "user",
|
|
5258
|
+
source_trust: sourceTrust
|
|
5259
|
+
});
|
|
5260
|
+
const callerMetadata = args.metadata;
|
|
5261
|
+
const metadataWithOrigin = {
|
|
5262
|
+
...callerMetadata ?? {},
|
|
5263
|
+
origin
|
|
5264
|
+
};
|
|
5265
|
+
const confidence = args.confidence !== undefined ? z.number().min(0).max(1).parse(args.confidence) : defaultConfidenceForSource({ source, source_trust: sourceTrust });
|
|
5266
|
+
const importance = args.importance !== undefined ? z.number().int().min(1).max(10).parse(args.importance) : importanceWithSignalBump(entityType, content);
|
|
5267
|
+
const projectIdForWrite = args.projectId || deps.getActiveProjectId() || undefined;
|
|
5268
|
+
let similar = [];
|
|
5269
|
+
if (!isSessionScope(entityScope)) {
|
|
5270
|
+
similar = await findSupersedeCandidates(client3, title, content, entityType, workspaceId, { projectId: projectIdForWrite, scope: entityScope });
|
|
5271
|
+
}
|
|
4947
5272
|
const result = await client3.createMemoryEntity({
|
|
4948
5273
|
workspace_id: workspaceId,
|
|
4949
|
-
project_id:
|
|
5274
|
+
project_id: projectIdForWrite,
|
|
4950
5275
|
type: entityType,
|
|
4951
5276
|
scope: entityScope,
|
|
4952
5277
|
memory_tier: args.tier || undefined,
|
|
4953
5278
|
title,
|
|
4954
5279
|
content,
|
|
4955
|
-
metadata:
|
|
4956
|
-
confidence
|
|
4957
|
-
importance
|
|
5280
|
+
metadata: metadataWithOrigin,
|
|
5281
|
+
confidence,
|
|
5282
|
+
importance,
|
|
4958
5283
|
tags: entityTags.length > 0 ? entityTags : undefined,
|
|
4959
5284
|
agent_identifier: activeMemSession?.agentIdentifier || undefined
|
|
4960
5285
|
});
|
|
5286
|
+
const supersedeId = args.supersedeId;
|
|
5287
|
+
const newEntityId = result.entity?.id;
|
|
5288
|
+
let superseded;
|
|
5289
|
+
if (supersedeId && newEntityId) {
|
|
5290
|
+
try {
|
|
5291
|
+
await client3.updateMemoryEntity(supersedeId, {
|
|
5292
|
+
superseded_by: newEntityId,
|
|
5293
|
+
superseded_at: new Date().toISOString()
|
|
5294
|
+
});
|
|
5295
|
+
superseded = supersedeId;
|
|
5296
|
+
} catch (err) {
|
|
5297
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5298
|
+
console.debug(`[harmony_remember] supersede ${supersedeId} failed: ${msg}`);
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
4961
5301
|
const newEntityIdForGraph = result.entity?.id;
|
|
4962
5302
|
if (newEntityIdForGraph) {
|
|
4963
|
-
autoExpandGraph(client3, newEntityIdForGraph, title, content, entityTags, workspaceId,
|
|
5303
|
+
autoExpandGraph(client3, newEntityIdForGraph, title, content, entityTags, workspaceId, projectIdForWrite).catch(() => {});
|
|
4964
5304
|
}
|
|
4965
5305
|
if (activeMemSession) {
|
|
4966
5306
|
appendMemoryAction(activeMemSession.cardId, `Stored memory: ${title}`);
|
|
@@ -4968,7 +5308,10 @@ async function handleToolCall(name, args, deps) {
|
|
|
4968
5308
|
}
|
|
4969
5309
|
return {
|
|
4970
5310
|
success: true,
|
|
4971
|
-
...result
|
|
5311
|
+
...result,
|
|
5312
|
+
...similar.length > 0 ? { similar } : {},
|
|
5313
|
+
...superseded ? { superseded } : {},
|
|
5314
|
+
...tagSuggestions.length > 0 ? { tagSuggestions } : {}
|
|
4972
5315
|
};
|
|
4973
5316
|
}
|
|
4974
5317
|
case "harmony_recall": {
|
|
@@ -4990,26 +5333,22 @@ async function handleToolCall(name, args, deps) {
|
|
|
4990
5333
|
let entities;
|
|
4991
5334
|
let relevanceMap;
|
|
4992
5335
|
if (queryText) {
|
|
5336
|
+
const requestedTags = args.tags;
|
|
4993
5337
|
const searchResult = await client3.searchMemoryEntities(workspaceId, queryText, {
|
|
4994
5338
|
project_id: projectId,
|
|
4995
5339
|
type: args.type,
|
|
4996
|
-
limit: fetchLimit
|
|
5340
|
+
limit: fetchLimit,
|
|
5341
|
+
tags: requestedTags && requestedTags.length > 0 ? requestedTags : undefined,
|
|
5342
|
+
include_superseded: includeSuperseded
|
|
4997
5343
|
});
|
|
4998
5344
|
entities = searchResult.entities ?? [];
|
|
4999
|
-
const requestedTags = args.tags;
|
|
5000
5345
|
const minConfidence = args.minConfidence;
|
|
5001
5346
|
if (userScopeFilter) {
|
|
5002
5347
|
entities = entities.filter((e) => e?.scope === userScopeFilter);
|
|
5003
5348
|
} else if (excludeSessionFromLongTerm) {
|
|
5004
5349
|
entities = entities.filter((e) => !isSessionScope(e?.scope));
|
|
5005
5350
|
}
|
|
5006
|
-
|
|
5007
|
-
const wanted = new Set(requestedTags);
|
|
5008
|
-
entities = entities.filter((e) => (e?.tags ?? []).some((t) => wanted.has(t)));
|
|
5009
|
-
}
|
|
5010
|
-
if (typeof minConfidence === "number") {
|
|
5011
|
-
entities = entities.filter((e) => typeof e?.confidence === "number" && e.confidence >= minConfidence);
|
|
5012
|
-
}
|
|
5351
|
+
entities = filterByMinConfidence(entities, minConfidence);
|
|
5013
5352
|
if (!includeSuperseded) {
|
|
5014
5353
|
entities = entities.filter((e) => !e?.superseded_at);
|
|
5015
5354
|
}
|
|
@@ -5040,16 +5379,10 @@ async function handleToolCall(name, args, deps) {
|
|
|
5040
5379
|
trimmed = fitToBudget(trimmed, budgetTokens);
|
|
5041
5380
|
}
|
|
5042
5381
|
if (trimmed.length > 0) {
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
...entity.metadata || {},
|
|
5048
|
-
_last_recall: new Date().toISOString()
|
|
5049
|
-
}
|
|
5050
|
-
});
|
|
5051
|
-
} catch (_) {}
|
|
5052
|
-
})).catch(() => {});
|
|
5382
|
+
const touchIds = trimmed.map(({ entity }) => entity?.id).filter((id) => typeof id === "string");
|
|
5383
|
+
if (touchIds.length > 0) {
|
|
5384
|
+
client3.batchTouchMemoryEntities(touchIds).catch(() => {});
|
|
5385
|
+
}
|
|
5053
5386
|
}
|
|
5054
5387
|
let sessionEntities = [];
|
|
5055
5388
|
if (excludeSessionFromLongTerm && recallMemSession?.agentSessionId) {
|
|
@@ -5116,8 +5449,12 @@ async function handleToolCall(name, args, deps) {
|
|
|
5116
5449
|
updates.type = args.type;
|
|
5117
5450
|
if (args.scope !== undefined)
|
|
5118
5451
|
updates.scope = args.scope;
|
|
5119
|
-
|
|
5120
|
-
|
|
5452
|
+
let updateTagSuggestions = [];
|
|
5453
|
+
if (args.tags !== undefined) {
|
|
5454
|
+
const updateRawTags = args.tags;
|
|
5455
|
+
updates.tags = normalizeTags(updateRawTags);
|
|
5456
|
+
updateTagSuggestions = lintTags(updateRawTags);
|
|
5457
|
+
}
|
|
5121
5458
|
if (args.confidence !== undefined)
|
|
5122
5459
|
updates.confidence = z.number().min(0).max(1).parse(args.confidence);
|
|
5123
5460
|
if (args.metadata !== undefined)
|
|
@@ -5129,7 +5466,11 @@ async function handleToolCall(name, args, deps) {
|
|
|
5129
5466
|
appendMemoryAction(updateMemSession.cardId, `Updated memory: ${updateTitle}`);
|
|
5130
5467
|
flushMemoryActions(client3, updateMemSession.cardId).catch(() => {});
|
|
5131
5468
|
}
|
|
5132
|
-
return {
|
|
5469
|
+
return {
|
|
5470
|
+
success: true,
|
|
5471
|
+
...result,
|
|
5472
|
+
...updateTagSuggestions.length > 0 ? { tagSuggestions: updateTagSuggestions } : {}
|
|
5473
|
+
};
|
|
5133
5474
|
}
|
|
5134
5475
|
case "harmony_forget": {
|
|
5135
5476
|
const entityId = z.string().uuid().parse(args.entityId);
|
|
@@ -5174,6 +5515,64 @@ async function handleToolCall(name, args, deps) {
|
|
|
5174
5515
|
}
|
|
5175
5516
|
return { success: true, ...result };
|
|
5176
5517
|
}
|
|
5518
|
+
case "harmony_suggest_relations": {
|
|
5519
|
+
const entityId = z.string().uuid().parse(args.entityId);
|
|
5520
|
+
const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
|
|
5521
|
+
if (!workspaceId) {
|
|
5522
|
+
throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
|
|
5523
|
+
}
|
|
5524
|
+
const projectId = args.projectId || deps.getActiveProjectId() || undefined;
|
|
5525
|
+
const limit = args.limit ?? 5;
|
|
5526
|
+
const { entity } = await client3.getMemoryEntity(entityId);
|
|
5527
|
+
const src = entity;
|
|
5528
|
+
if (!src) {
|
|
5529
|
+
throw new Error(`Entity not found: ${entityId}`);
|
|
5530
|
+
}
|
|
5531
|
+
const related = await client3.getRelatedEntities(entityId);
|
|
5532
|
+
const excludeIds = new Set([entityId]);
|
|
5533
|
+
for (const raw of [
|
|
5534
|
+
...related.outgoing || [],
|
|
5535
|
+
...related.incoming || []
|
|
5536
|
+
]) {
|
|
5537
|
+
const rel = raw;
|
|
5538
|
+
const target = rel.target;
|
|
5539
|
+
const source = rel.source;
|
|
5540
|
+
const otherId = target?.id ?? rel.target_id ?? source?.id ?? rel.source_id;
|
|
5541
|
+
if (otherId)
|
|
5542
|
+
excludeIds.add(otherId);
|
|
5543
|
+
}
|
|
5544
|
+
const suggestions = await findSimilarEntities(client3, src.title ?? "", src.content ?? "", workspaceId, {
|
|
5545
|
+
projectId,
|
|
5546
|
+
limit: limit + excludeIds.size,
|
|
5547
|
+
excludeIds: [...excludeIds]
|
|
5548
|
+
});
|
|
5549
|
+
return {
|
|
5550
|
+
success: true,
|
|
5551
|
+
suggestions: suggestions.slice(0, limit).map((s) => ({
|
|
5552
|
+
id: s.id,
|
|
5553
|
+
title: s.title,
|
|
5554
|
+
type: s.type,
|
|
5555
|
+
rrf_score: s.rrf_score
|
|
5556
|
+
}))
|
|
5557
|
+
};
|
|
5558
|
+
}
|
|
5559
|
+
case "harmony_recall_feedback": {
|
|
5560
|
+
const entityId = z.string().uuid().parse(args.entityId);
|
|
5561
|
+
const vote = z.enum(["up", "down"]).parse(args.vote);
|
|
5562
|
+
const { entity } = await client3.getMemoryEntity(entityId);
|
|
5563
|
+
const existingMeta = entity?.metadata ?? {};
|
|
5564
|
+
const existingFeedback = existingMeta.feedback;
|
|
5565
|
+
const feedback = mergeFeedback(existingFeedback, vote);
|
|
5566
|
+
const result = await client3.updateMemoryEntity(entityId, {
|
|
5567
|
+
metadata: { ...existingMeta, feedback }
|
|
5568
|
+
});
|
|
5569
|
+
const fbMemSession = getActiveMemorySession();
|
|
5570
|
+
if (fbMemSession) {
|
|
5571
|
+
appendMemoryAction(fbMemSession.cardId, `Memory feedback: ${vote === "up" ? "\uD83D\uDC4D" : "\uD83D\uDC4E"} ${entityId.slice(0, 8)}`);
|
|
5572
|
+
flushMemoryActions(client3, fbMemSession.cardId).catch(() => {});
|
|
5573
|
+
}
|
|
5574
|
+
return { success: true, feedback, ...result };
|
|
5575
|
+
}
|
|
5177
5576
|
case "harmony_memory_search": {
|
|
5178
5577
|
const query = z.string().min(1).max(500).parse(args.query);
|
|
5179
5578
|
const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
|