@gethmy/mcp 2.8.3 → 2.8.5
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 +481 -36
- package/dist/index.js +481 -36
- package/dist/lib/api-client.js +17 -4
- package/package.json +1 -1
- package/src/api-client.ts +8 -0
- package/src/auto-session.ts +19 -9
- package/src/graph-expansion.ts +153 -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 +379 -43
package/dist/index.js
CHANGED
|
@@ -934,10 +934,14 @@ function getDisplayLinkType(linkType, direction) {
|
|
|
934
934
|
}
|
|
935
935
|
// ../harmony-shared/dist/commentSerializer.js
|
|
936
936
|
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.";
|
|
937
|
+
function sanitizeHeaderField(value) {
|
|
938
|
+
return value.replace(/[\]\r\n|<>]/g, " ").trim() || "—";
|
|
939
|
+
}
|
|
937
940
|
function authorLabel(c) {
|
|
938
941
|
if (c.author_type === "agent")
|
|
939
942
|
return "AI agent";
|
|
940
|
-
|
|
943
|
+
const raw = c.author?.full_name || "teammate";
|
|
944
|
+
return sanitizeHeaderField(raw);
|
|
941
945
|
}
|
|
942
946
|
function criticalIds(comments) {
|
|
943
947
|
const keep = new Set;
|
|
@@ -994,9 +998,15 @@ function serializeCommentThread(comments, options = {}) {
|
|
|
994
998
|
if (c.resolved_at)
|
|
995
999
|
tags.push("resolved");
|
|
996
1000
|
const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
|
|
997
|
-
const header = `[${ref(c.id)} | ${c.author_type} | ${authorLabel(c)} | ${c.comment_type} | ${c.created_at}${tagStr}]`;
|
|
998
|
-
|
|
999
|
-
|
|
1001
|
+
const header = `[${sanitizeHeaderField(ref(c.id))} | ${sanitizeHeaderField(c.author_type)} | ${authorLabel(c)} | ${sanitizeHeaderField(c.comment_type)} | ${sanitizeHeaderField(c.created_at)}${tagStr}]`;
|
|
1002
|
+
const fencedBody = c.body.trim().replaceAll("<", "<").replaceAll(">", ">");
|
|
1003
|
+
lines.push({
|
|
1004
|
+
at: c.created_at,
|
|
1005
|
+
text: `${header}
|
|
1006
|
+
<comment-body>
|
|
1007
|
+
${fencedBody}
|
|
1008
|
+
</comment-body>`
|
|
1009
|
+
});
|
|
1000
1010
|
}
|
|
1001
1011
|
for (const a of activity) {
|
|
1002
1012
|
const actor = a.actor ? `${a.actor} ` : "";
|
|
@@ -1547,6 +1557,9 @@ class HarmonyApiClient {
|
|
|
1547
1557
|
async toggleSubtask(subtaskId) {
|
|
1548
1558
|
return this.request("POST", `/subtasks/${subtaskId}/toggle`);
|
|
1549
1559
|
}
|
|
1560
|
+
async updateSubtask(subtaskId, updates) {
|
|
1561
|
+
return this.request("PATCH", `/subtasks/${subtaskId}`, updates);
|
|
1562
|
+
}
|
|
1550
1563
|
async deleteSubtask(subtaskId) {
|
|
1551
1564
|
return this.request("DELETE", `/subtasks/${subtaskId}`);
|
|
1552
1565
|
}
|
|
@@ -2020,9 +2033,9 @@ function resolveAgentIdentity(info) {
|
|
|
2020
2033
|
}
|
|
2021
2034
|
var AUTO_START_TRIGGERS = new Set([
|
|
2022
2035
|
"harmony_generate_prompt",
|
|
2023
|
-
"harmony_update_card",
|
|
2024
2036
|
"harmony_create_subtask",
|
|
2025
|
-
"harmony_toggle_subtask"
|
|
2037
|
+
"harmony_toggle_subtask",
|
|
2038
|
+
"harmony_update_subtask"
|
|
2026
2039
|
]);
|
|
2027
2040
|
var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
2028
2041
|
var CHECK_INTERVAL_MS = 60 * 1000;
|
|
@@ -2051,6 +2064,10 @@ async function trackActivity(cardId, options) {
|
|
|
2051
2064
|
const client3 = options?.client ?? clientGetter?.();
|
|
2052
2065
|
if (!client3)
|
|
2053
2066
|
return;
|
|
2067
|
+
const info = clientInfoGetter?.() ?? null;
|
|
2068
|
+
if (!info?.name)
|
|
2069
|
+
return;
|
|
2070
|
+
const { agentIdentifier, agentName } = resolveAgentIdentity(info);
|
|
2054
2071
|
const toEnd = [];
|
|
2055
2072
|
for (const [otherCardId, session] of activeSessions) {
|
|
2056
2073
|
if (otherCardId !== cardId && !session.isExplicit) {
|
|
@@ -2060,8 +2077,6 @@ async function trackActivity(cardId, options) {
|
|
|
2060
2077
|
for (const otherCardId of toEnd) {
|
|
2061
2078
|
await autoEndSession(client3, otherCardId, "completed");
|
|
2062
2079
|
}
|
|
2063
|
-
const info = clientInfoGetter?.() ?? null;
|
|
2064
|
-
const { agentIdentifier, agentName } = resolveAgentIdentity(info);
|
|
2065
2080
|
try {
|
|
2066
2081
|
await client3.startAgentSession(cardId, {
|
|
2067
2082
|
agentIdentifier,
|
|
@@ -2181,6 +2196,90 @@ async function autoExpandGraph(client3, entityId, title, content, _tags, workspa
|
|
|
2181
2196
|
return { relationsCreated: 0 };
|
|
2182
2197
|
}
|
|
2183
2198
|
}
|
|
2199
|
+
async function findSimilarEntities(client3, title, content, workspaceId, options) {
|
|
2200
|
+
const contentSnippet = content.slice(0, 200).trim();
|
|
2201
|
+
const query = [title, contentSnippet].filter(Boolean).join(" ");
|
|
2202
|
+
try {
|
|
2203
|
+
const { entities } = await client3.searchMemoryEntities(workspaceId, query, {
|
|
2204
|
+
project_id: options?.projectId,
|
|
2205
|
+
limit: options?.limit ?? 20,
|
|
2206
|
+
type: options?.type
|
|
2207
|
+
});
|
|
2208
|
+
const minScore = options?.minRrfScore ?? 0;
|
|
2209
|
+
const excludeSet = new Set(options?.excludeIds || []);
|
|
2210
|
+
return entities.filter((e) => {
|
|
2211
|
+
if (excludeSet.has(e.id))
|
|
2212
|
+
return false;
|
|
2213
|
+
if (minScore > 0 && (e.rrf_score ?? 0) < minScore)
|
|
2214
|
+
return false;
|
|
2215
|
+
return true;
|
|
2216
|
+
});
|
|
2217
|
+
} catch {
|
|
2218
|
+
return [];
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
var SUPERSEDE_RRF_THRESHOLD = 0.029;
|
|
2222
|
+
var SUPERSEDE_TITLE_OVERLAP = 0.5;
|
|
2223
|
+
var TITLE_STOPWORDS = new Set([
|
|
2224
|
+
"a",
|
|
2225
|
+
"an",
|
|
2226
|
+
"the",
|
|
2227
|
+
"and",
|
|
2228
|
+
"or",
|
|
2229
|
+
"of",
|
|
2230
|
+
"to",
|
|
2231
|
+
"in",
|
|
2232
|
+
"on",
|
|
2233
|
+
"for",
|
|
2234
|
+
"with",
|
|
2235
|
+
"is",
|
|
2236
|
+
"are",
|
|
2237
|
+
"be",
|
|
2238
|
+
"by",
|
|
2239
|
+
"at",
|
|
2240
|
+
"as"
|
|
2241
|
+
]);
|
|
2242
|
+
function significantTitleTokens(title) {
|
|
2243
|
+
return new Set(title.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 2 && !TITLE_STOPWORDS.has(t)));
|
|
2244
|
+
}
|
|
2245
|
+
function jaccard(a, b) {
|
|
2246
|
+
if (a.size === 0 || b.size === 0)
|
|
2247
|
+
return 0;
|
|
2248
|
+
let intersection = 0;
|
|
2249
|
+
for (const t of a)
|
|
2250
|
+
if (b.has(t))
|
|
2251
|
+
intersection++;
|
|
2252
|
+
return intersection / (a.size + b.size - intersection);
|
|
2253
|
+
}
|
|
2254
|
+
async function findSupersedeCandidates(client3, title, content, type, workspaceId, options) {
|
|
2255
|
+
const rrfThreshold = options?.rrfThreshold ?? SUPERSEDE_RRF_THRESHOLD;
|
|
2256
|
+
const titleOverlap = options?.titleOverlap ?? SUPERSEDE_TITLE_OVERLAP;
|
|
2257
|
+
const candidateTokens = significantTitleTokens(title);
|
|
2258
|
+
try {
|
|
2259
|
+
const hits = await findSimilarEntities(client3, title, content, workspaceId, {
|
|
2260
|
+
projectId: options?.projectId,
|
|
2261
|
+
type,
|
|
2262
|
+
limit: options?.limit ?? 10,
|
|
2263
|
+
minRrfScore: rrfThreshold
|
|
2264
|
+
});
|
|
2265
|
+
return hits.filter((e) => {
|
|
2266
|
+
if (options?.scope && e.scope !== undefined) {
|
|
2267
|
+
if (e.scope !== options.scope)
|
|
2268
|
+
return false;
|
|
2269
|
+
}
|
|
2270
|
+
if (e.superseded_at) {
|
|
2271
|
+
return false;
|
|
2272
|
+
}
|
|
2273
|
+
return jaccard(candidateTokens, significantTitleTokens(e.title)) >= titleOverlap;
|
|
2274
|
+
}).map((e) => ({
|
|
2275
|
+
id: e.id,
|
|
2276
|
+
title: e.title,
|
|
2277
|
+
score: e.rrf_score ?? 0
|
|
2278
|
+
}));
|
|
2279
|
+
} catch {
|
|
2280
|
+
return [];
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2184
2283
|
|
|
2185
2284
|
// src/memory-floor.ts
|
|
2186
2285
|
var STOP_WORDS = new Set([
|
|
@@ -2319,6 +2418,10 @@ var DEFAULT_WEIGHTS = {
|
|
|
2319
2418
|
recency: 0.25,
|
|
2320
2419
|
importance: 0.2
|
|
2321
2420
|
};
|
|
2421
|
+
var USAGE_BUMP_SCALE = 0.6;
|
|
2422
|
+
var USAGE_BUMP_MAX = 2;
|
|
2423
|
+
var FEEDBACK_BUMP_SCALE = 0.8;
|
|
2424
|
+
var FEEDBACK_BUMP_MAX = 2;
|
|
2322
2425
|
var TYPE_TAU_SECONDS = {
|
|
2323
2426
|
preference: Number.POSITIVE_INFINITY,
|
|
2324
2427
|
pattern: 60 * 60 * 24 * 180,
|
|
@@ -2374,8 +2477,37 @@ function recencyDecay(lastAccessedAt, createdAt, type, now) {
|
|
|
2374
2477
|
const dtSec = Math.max(0, (now.getTime() - t) / 1000);
|
|
2375
2478
|
return clamp01(Math.exp(-dtSec / tau));
|
|
2376
2479
|
}
|
|
2377
|
-
function
|
|
2480
|
+
function baseImportance(raw, type) {
|
|
2378
2481
|
let v = typeof raw === "number" ? raw : TYPE_IMPORTANCE_DEFAULT[type] ?? 5;
|
|
2482
|
+
if (v < 1)
|
|
2483
|
+
v = 1;
|
|
2484
|
+
if (v > 10)
|
|
2485
|
+
v = 10;
|
|
2486
|
+
return v;
|
|
2487
|
+
}
|
|
2488
|
+
function usageBump(accessCount) {
|
|
2489
|
+
const n = typeof accessCount === "number" && accessCount > 0 ? accessCount : 0;
|
|
2490
|
+
if (n === 0)
|
|
2491
|
+
return 0;
|
|
2492
|
+
return Math.min(USAGE_BUMP_MAX, USAGE_BUMP_SCALE * Math.log(1 + n));
|
|
2493
|
+
}
|
|
2494
|
+
function feedbackBump(feedback) {
|
|
2495
|
+
const up = typeof feedback?.up === "number" ? feedback.up : 0;
|
|
2496
|
+
const down = typeof feedback?.down === "number" ? feedback.down : 0;
|
|
2497
|
+
const net = up - down;
|
|
2498
|
+
if (net === 0)
|
|
2499
|
+
return 0;
|
|
2500
|
+
const raw = FEEDBACK_BUMP_SCALE * Math.sign(net) * Math.log(1 + Math.abs(net));
|
|
2501
|
+
if (raw > FEEDBACK_BUMP_MAX)
|
|
2502
|
+
return FEEDBACK_BUMP_MAX;
|
|
2503
|
+
if (raw < -FEEDBACK_BUMP_MAX)
|
|
2504
|
+
return -FEEDBACK_BUMP_MAX;
|
|
2505
|
+
return raw;
|
|
2506
|
+
}
|
|
2507
|
+
function effectiveImportance(entity) {
|
|
2508
|
+
const base = baseImportance(entity.importance, entity.type);
|
|
2509
|
+
const bump = usageBump(entity.access_count) + feedbackBump(entity.metadata?.feedback);
|
|
2510
|
+
let v = base + bump;
|
|
2379
2511
|
if (v < 1)
|
|
2380
2512
|
v = 1;
|
|
2381
2513
|
if (v > 10)
|
|
@@ -2389,7 +2521,7 @@ function rescore(entities, options = {}) {
|
|
|
2389
2521
|
const scored = entities.map((entity) => {
|
|
2390
2522
|
const relevance = clamp01(relevanceMap.get(entity.id ?? "") ?? 0.5);
|
|
2391
2523
|
const recency = recencyDecay(entity.last_accessed_at, entity.created_at, entity.type, now);
|
|
2392
|
-
const importance =
|
|
2524
|
+
const importance = effectiveImportance(entity);
|
|
2393
2525
|
const score = w.relevance * relevance + w.recency * recency + w.importance * importance;
|
|
2394
2526
|
return { entity, relevance, recency, importance, score };
|
|
2395
2527
|
});
|
|
@@ -2402,6 +2534,11 @@ function rescore(entities, options = {}) {
|
|
|
2402
2534
|
});
|
|
2403
2535
|
return scored;
|
|
2404
2536
|
}
|
|
2537
|
+
function filterByMinConfidence(entities, minConfidence) {
|
|
2538
|
+
if (typeof minConfidence !== "number")
|
|
2539
|
+
return entities;
|
|
2540
|
+
return entities.filter((e) => typeof e.confidence === "number" && e.confidence >= minConfidence);
|
|
2541
|
+
}
|
|
2405
2542
|
function relevanceFromRank(ranked, decay = 10) {
|
|
2406
2543
|
const out = new Map;
|
|
2407
2544
|
ranked.forEach((entity, rank) => {
|
|
@@ -2434,6 +2571,82 @@ function fitToBudget(scored, budgetTokens) {
|
|
|
2434
2571
|
}
|
|
2435
2572
|
return out;
|
|
2436
2573
|
}
|
|
2574
|
+
function mergeFeedback(existing, vote) {
|
|
2575
|
+
const up = typeof existing?.up === "number" && existing.up > 0 ? existing.up : 0;
|
|
2576
|
+
const down = typeof existing?.down === "number" && existing.down > 0 ? existing.down : 0;
|
|
2577
|
+
return vote === "up" ? { up: up + 1, down } : { up, down: down + 1 };
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
// src/memory-provenance.ts
|
|
2581
|
+
function buildOrigin(input) {
|
|
2582
|
+
const origin = { source: input.source };
|
|
2583
|
+
if (input.source_card_id)
|
|
2584
|
+
origin.source_card_id = input.source_card_id;
|
|
2585
|
+
if (input.source_session_id)
|
|
2586
|
+
origin.source_session_id = input.source_session_id;
|
|
2587
|
+
if (input.author)
|
|
2588
|
+
origin.author = input.author;
|
|
2589
|
+
if (input.source_trust)
|
|
2590
|
+
origin.source_trust = input.source_trust;
|
|
2591
|
+
return origin;
|
|
2592
|
+
}
|
|
2593
|
+
function defaultConfidenceForSource(input) {
|
|
2594
|
+
if (input.source_trust === "document")
|
|
2595
|
+
return 0.9;
|
|
2596
|
+
if (input.source_trust === "manual")
|
|
2597
|
+
return 0.9;
|
|
2598
|
+
switch (input.source) {
|
|
2599
|
+
case "manual":
|
|
2600
|
+
return 0.9;
|
|
2601
|
+
case "assistant":
|
|
2602
|
+
return 0.8;
|
|
2603
|
+
case "agent-run":
|
|
2604
|
+
return 0.6;
|
|
2605
|
+
case "import":
|
|
2606
|
+
return 0.6;
|
|
2607
|
+
default:
|
|
2608
|
+
return 0.7;
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
var TYPE_IMPORTANCE_DEFAULT2 = {
|
|
2612
|
+
preference: 9,
|
|
2613
|
+
lesson: 8,
|
|
2614
|
+
decision: 8,
|
|
2615
|
+
pattern: 7,
|
|
2616
|
+
solution: 7,
|
|
2617
|
+
procedure: 7,
|
|
2618
|
+
error: 5,
|
|
2619
|
+
context: 5,
|
|
2620
|
+
task: 5,
|
|
2621
|
+
agent: 5,
|
|
2622
|
+
relationship: 6,
|
|
2623
|
+
commitment: 7,
|
|
2624
|
+
project: 6,
|
|
2625
|
+
handoff: 6
|
|
2626
|
+
};
|
|
2627
|
+
var FILE_PATH = /(?:[\w.-]+\/){1,}[\w.-]+\.[a-z]{1,4}\b|(?:[\w-]+\/){2,}[\w-]+/i;
|
|
2628
|
+
var GUIDANCE_SECTION = /\b(why|how to apply|how to use|takeaway|root cause)\b/i;
|
|
2629
|
+
var PROPER_NOUN = /`[^`]+`|\b[A-Z][a-z0-9]+[A-Z][A-Za-z0-9]*\b|\b[a-z]+_[a-z][\w]*\b/;
|
|
2630
|
+
function signalDensity(content) {
|
|
2631
|
+
let n = 0;
|
|
2632
|
+
if (FILE_PATH.test(content))
|
|
2633
|
+
n += 1;
|
|
2634
|
+
if (GUIDANCE_SECTION.test(content))
|
|
2635
|
+
n += 1;
|
|
2636
|
+
if (PROPER_NOUN.test(content))
|
|
2637
|
+
n += 1;
|
|
2638
|
+
return n;
|
|
2639
|
+
}
|
|
2640
|
+
function importanceWithSignalBump(type, content) {
|
|
2641
|
+
const base = TYPE_IMPORTANCE_DEFAULT2[type] ?? 5;
|
|
2642
|
+
const bump = Math.min(2, signalDensity(content));
|
|
2643
|
+
const v = base + bump;
|
|
2644
|
+
if (v < 1)
|
|
2645
|
+
return 1;
|
|
2646
|
+
if (v > 10)
|
|
2647
|
+
return 10;
|
|
2648
|
+
return v;
|
|
2649
|
+
}
|
|
2437
2650
|
|
|
2438
2651
|
// src/memory-session.ts
|
|
2439
2652
|
var SESSION_SCOPE_PREFIX = "session:";
|
|
@@ -2454,6 +2667,49 @@ function sessionScopeFor(agentSessionId) {
|
|
|
2454
2667
|
return `${SESSION_SCOPE_PREFIX}${agentSessionId}`;
|
|
2455
2668
|
}
|
|
2456
2669
|
|
|
2670
|
+
// src/memory-tags.ts
|
|
2671
|
+
var CARD_REF = /^card[\s:#-]+(\d+)$/;
|
|
2672
|
+
function normalizeTag(tag) {
|
|
2673
|
+
const cleaned = tag.trim().toLowerCase().replace(/\s+/g, " ");
|
|
2674
|
+
const cardMatch = cleaned.match(CARD_REF);
|
|
2675
|
+
if (cardMatch) {
|
|
2676
|
+
return `card:${cardMatch[1]}`;
|
|
2677
|
+
}
|
|
2678
|
+
return cleaned;
|
|
2679
|
+
}
|
|
2680
|
+
function normalizeTags(tags) {
|
|
2681
|
+
const seen = new Set;
|
|
2682
|
+
const out = [];
|
|
2683
|
+
for (const raw of tags) {
|
|
2684
|
+
const norm = normalizeTag(raw);
|
|
2685
|
+
if (!norm || seen.has(norm))
|
|
2686
|
+
continue;
|
|
2687
|
+
seen.add(norm);
|
|
2688
|
+
out.push(norm);
|
|
2689
|
+
}
|
|
2690
|
+
return out;
|
|
2691
|
+
}
|
|
2692
|
+
function lintTags(tags) {
|
|
2693
|
+
const suggestions = [];
|
|
2694
|
+
for (const tag of tags) {
|
|
2695
|
+
const norm = normalizeTag(tag);
|
|
2696
|
+
if (norm === tag)
|
|
2697
|
+
continue;
|
|
2698
|
+
let reason;
|
|
2699
|
+
if (CARD_REF.test(tag.trim().toLowerCase()) && norm.startsWith("card:")) {
|
|
2700
|
+
reason = "non-canonical card ref; use 'card:<n>'";
|
|
2701
|
+
} else if (tag !== tag.trim() || /\s{2,}/.test(tag)) {
|
|
2702
|
+
reason = "whitespace; trim and collapse";
|
|
2703
|
+
} else if (tag !== tag.toLowerCase()) {
|
|
2704
|
+
reason = "case variant; use lowercase";
|
|
2705
|
+
} else {
|
|
2706
|
+
reason = "normalize to canonical form";
|
|
2707
|
+
}
|
|
2708
|
+
suggestions.push({ tag, suggestion: norm, reason });
|
|
2709
|
+
}
|
|
2710
|
+
return suggestions;
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2457
2713
|
// src/onboard.ts
|
|
2458
2714
|
async function onboardNewUser(params) {
|
|
2459
2715
|
const {
|
|
@@ -2940,7 +3196,12 @@ var TOOLS = {
|
|
|
2940
3196
|
priority: { type: "string", enum: ["low", "medium", "high", "urgent"] },
|
|
2941
3197
|
assigneeId: { type: "string", nullable: true },
|
|
2942
3198
|
dueDate: { type: "string", nullable: true },
|
|
2943
|
-
done: { type: "boolean", description: "Mark card as done or not done" }
|
|
3199
|
+
done: { type: "boolean", description: "Mark card as done or not done" },
|
|
3200
|
+
planId: {
|
|
3201
|
+
type: "string",
|
|
3202
|
+
nullable: true,
|
|
3203
|
+
description: "Plan ID to link this card to, or null to unlink. Sets cards.plan_id. The plan must exist and belong to the same project."
|
|
3204
|
+
}
|
|
2944
3205
|
},
|
|
2945
3206
|
required: ["cardId"]
|
|
2946
3207
|
}
|
|
@@ -3239,6 +3500,25 @@ var TOOLS = {
|
|
|
3239
3500
|
required: ["subtaskId"]
|
|
3240
3501
|
}
|
|
3241
3502
|
},
|
|
3503
|
+
harmony_update_subtask: {
|
|
3504
|
+
description: "Update a subtask: rename its title, set an explicit completion state, and/or reorder it. Unlike harmony_toggle_subtask (which flips completion), `completed` here sets an explicit value — safe to call idempotently. At least one of title/completed/position is required.",
|
|
3505
|
+
inputSchema: {
|
|
3506
|
+
type: "object",
|
|
3507
|
+
properties: {
|
|
3508
|
+
subtaskId: { type: "string" },
|
|
3509
|
+
title: { type: "string", description: "New title (1–500 chars)" },
|
|
3510
|
+
completed: {
|
|
3511
|
+
type: "boolean",
|
|
3512
|
+
description: "Explicit completion state (set, not toggle)"
|
|
3513
|
+
},
|
|
3514
|
+
position: {
|
|
3515
|
+
type: "number",
|
|
3516
|
+
description: "New position (0-based ordering within the card)"
|
|
3517
|
+
}
|
|
3518
|
+
},
|
|
3519
|
+
required: ["subtaskId"]
|
|
3520
|
+
}
|
|
3521
|
+
},
|
|
3242
3522
|
harmony_delete_subtask: {
|
|
3243
3523
|
description: "Delete a subtask",
|
|
3244
3524
|
inputSchema: {
|
|
@@ -3647,6 +3927,10 @@ var TOOLS = {
|
|
|
3647
3927
|
projectId: {
|
|
3648
3928
|
type: "string",
|
|
3649
3929
|
description: "Project ID (optional, required if scope is 'project')"
|
|
3930
|
+
},
|
|
3931
|
+
supersedeId: {
|
|
3932
|
+
type: "string",
|
|
3933
|
+
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."
|
|
3650
3934
|
}
|
|
3651
3935
|
},
|
|
3652
3936
|
required: ["title", "content"]
|
|
@@ -3820,6 +4104,49 @@ var TOOLS = {
|
|
|
3820
4104
|
required: ["sourceId", "targetId", "relationType"]
|
|
3821
4105
|
}
|
|
3822
4106
|
},
|
|
4107
|
+
harmony_suggest_relations: {
|
|
4108
|
+
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.",
|
|
4109
|
+
inputSchema: {
|
|
4110
|
+
type: "object",
|
|
4111
|
+
properties: {
|
|
4112
|
+
entityId: {
|
|
4113
|
+
type: "string",
|
|
4114
|
+
description: "The entity UUID to find related candidates for"
|
|
4115
|
+
},
|
|
4116
|
+
limit: {
|
|
4117
|
+
type: "number",
|
|
4118
|
+
description: "Max suggestions to return (default: 5)"
|
|
4119
|
+
},
|
|
4120
|
+
workspaceId: {
|
|
4121
|
+
type: "string",
|
|
4122
|
+
description: "Workspace ID (optional if context set)"
|
|
4123
|
+
},
|
|
4124
|
+
projectId: {
|
|
4125
|
+
type: "string",
|
|
4126
|
+
description: "Project ID (optional)"
|
|
4127
|
+
}
|
|
4128
|
+
},
|
|
4129
|
+
required: ["entityId"]
|
|
4130
|
+
}
|
|
4131
|
+
},
|
|
4132
|
+
harmony_recall_feedback: {
|
|
4133
|
+
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').",
|
|
4134
|
+
inputSchema: {
|
|
4135
|
+
type: "object",
|
|
4136
|
+
properties: {
|
|
4137
|
+
entityId: {
|
|
4138
|
+
type: "string",
|
|
4139
|
+
description: "Memory entity UUID that was recalled"
|
|
4140
|
+
},
|
|
4141
|
+
vote: {
|
|
4142
|
+
type: "string",
|
|
4143
|
+
enum: ["up", "down"],
|
|
4144
|
+
description: "'up' if the memory was useful, 'down' if it was misleading or irrelevant"
|
|
4145
|
+
}
|
|
4146
|
+
},
|
|
4147
|
+
required: ["entityId", "vote"]
|
|
4148
|
+
}
|
|
4149
|
+
},
|
|
3823
4150
|
harmony_memory_search: {
|
|
3824
4151
|
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.",
|
|
3825
4152
|
inputSchema: {
|
|
@@ -4356,13 +4683,15 @@ async function handleToolCall(name, args, deps) {
|
|
|
4356
4683
|
}
|
|
4357
4684
|
case "harmony_update_card": {
|
|
4358
4685
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
4686
|
+
const planId = args.planId === undefined ? undefined : args.planId === null ? null : z.string().uuid().parse(args.planId);
|
|
4359
4687
|
const result = await client3.updateCard(cardId, {
|
|
4360
4688
|
title: args.title,
|
|
4361
4689
|
description: args.description,
|
|
4362
4690
|
priority: args.priority,
|
|
4363
4691
|
assigneeId: args.assigneeId,
|
|
4364
4692
|
dueDate: args.dueDate,
|
|
4365
|
-
done: args.done
|
|
4693
|
+
done: args.done,
|
|
4694
|
+
planId
|
|
4366
4695
|
});
|
|
4367
4696
|
return { success: true, ...result };
|
|
4368
4697
|
}
|
|
@@ -4565,6 +4894,24 @@ async function handleToolCall(name, args, deps) {
|
|
|
4565
4894
|
const result = await client3.toggleSubtask(subtaskId);
|
|
4566
4895
|
return { success: true, ...result };
|
|
4567
4896
|
}
|
|
4897
|
+
case "harmony_update_subtask": {
|
|
4898
|
+
const subtaskId = z.string().uuid().parse(args.subtaskId);
|
|
4899
|
+
const updates = {};
|
|
4900
|
+
if (args.title !== undefined) {
|
|
4901
|
+
updates.title = z.string().min(1).max(500).parse(args.title);
|
|
4902
|
+
}
|
|
4903
|
+
if (args.completed !== undefined) {
|
|
4904
|
+
updates.completed = z.boolean().parse(args.completed);
|
|
4905
|
+
}
|
|
4906
|
+
if (args.position !== undefined) {
|
|
4907
|
+
updates.position = z.number().int().min(0).parse(args.position);
|
|
4908
|
+
}
|
|
4909
|
+
if (Object.keys(updates).length === 0) {
|
|
4910
|
+
throw new Error("harmony_update_subtask requires at least one of: title, completed, position");
|
|
4911
|
+
}
|
|
4912
|
+
const result = await client3.updateSubtask(subtaskId, updates);
|
|
4913
|
+
return { success: true, ...result };
|
|
4914
|
+
}
|
|
4568
4915
|
case "harmony_delete_subtask": {
|
|
4569
4916
|
const subtaskId = z.string().uuid().parse(args.subtaskId);
|
|
4570
4917
|
await client3.deleteSubtask(subtaskId);
|
|
@@ -4877,7 +5224,9 @@ async function handleToolCall(name, args, deps) {
|
|
|
4877
5224
|
throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
|
|
4878
5225
|
}
|
|
4879
5226
|
const entityType = args.type || "context";
|
|
4880
|
-
const
|
|
5227
|
+
const rawTags = args.tags || [];
|
|
5228
|
+
const entityTags = normalizeTags(rawTags);
|
|
5229
|
+
const tagSuggestions = lintTags(rawTags);
|
|
4881
5230
|
const activeMemSession = getActiveMemorySession();
|
|
4882
5231
|
const entityScope = resolveSessionScope(args.scope, activeMemSession?.agentSessionId) || "project";
|
|
4883
5232
|
const floorRejection = validateMemoryQuality({
|
|
@@ -4891,24 +5240,59 @@ async function handleToolCall(name, args, deps) {
|
|
|
4891
5240
|
if (floorRejection) {
|
|
4892
5241
|
throw new Error(`Memory rejected by Utility Floor [${floorRejection.rule}]: ${floorRejection.message}`);
|
|
4893
5242
|
}
|
|
4894
|
-
const
|
|
5243
|
+
const sourceTrust = args.source_trust;
|
|
5244
|
+
const source = activeMemSession ? activeMemSession.agentIdentifier === "assistant" ? "assistant" : "agent-run" : "manual";
|
|
5245
|
+
const origin = buildOrigin({
|
|
5246
|
+
source,
|
|
5247
|
+
source_card_id: activeMemSession?.cardId,
|
|
5248
|
+
source_session_id: activeMemSession?.agentSessionId,
|
|
5249
|
+
author: activeMemSession?.agentIdentifier ?? "user",
|
|
5250
|
+
source_trust: sourceTrust
|
|
5251
|
+
});
|
|
5252
|
+
const callerMetadata = args.metadata;
|
|
5253
|
+
const metadataWithOrigin = {
|
|
5254
|
+
...callerMetadata ?? {},
|
|
5255
|
+
origin
|
|
5256
|
+
};
|
|
5257
|
+
const confidence = args.confidence !== undefined ? z.number().min(0).max(1).parse(args.confidence) : defaultConfidenceForSource({ source, source_trust: sourceTrust });
|
|
5258
|
+
const importance = args.importance !== undefined ? z.number().int().min(1).max(10).parse(args.importance) : importanceWithSignalBump(entityType, content);
|
|
5259
|
+
const projectIdForWrite = args.projectId || deps.getActiveProjectId() || undefined;
|
|
5260
|
+
let similar = [];
|
|
5261
|
+
if (!isSessionScope(entityScope)) {
|
|
5262
|
+
similar = await findSupersedeCandidates(client3, title, content, entityType, workspaceId, { projectId: projectIdForWrite, scope: entityScope });
|
|
5263
|
+
}
|
|
4895
5264
|
const result = await client3.createMemoryEntity({
|
|
4896
5265
|
workspace_id: workspaceId,
|
|
4897
|
-
project_id:
|
|
5266
|
+
project_id: projectIdForWrite,
|
|
4898
5267
|
type: entityType,
|
|
4899
5268
|
scope: entityScope,
|
|
4900
5269
|
memory_tier: args.tier || undefined,
|
|
4901
5270
|
title,
|
|
4902
5271
|
content,
|
|
4903
|
-
metadata:
|
|
4904
|
-
confidence
|
|
4905
|
-
importance
|
|
5272
|
+
metadata: metadataWithOrigin,
|
|
5273
|
+
confidence,
|
|
5274
|
+
importance,
|
|
4906
5275
|
tags: entityTags.length > 0 ? entityTags : undefined,
|
|
4907
5276
|
agent_identifier: activeMemSession?.agentIdentifier || undefined
|
|
4908
5277
|
});
|
|
5278
|
+
const supersedeId = args.supersedeId;
|
|
5279
|
+
const newEntityId = result.entity?.id;
|
|
5280
|
+
let superseded;
|
|
5281
|
+
if (supersedeId && newEntityId) {
|
|
5282
|
+
try {
|
|
5283
|
+
await client3.updateMemoryEntity(supersedeId, {
|
|
5284
|
+
superseded_by: newEntityId,
|
|
5285
|
+
superseded_at: new Date().toISOString()
|
|
5286
|
+
});
|
|
5287
|
+
superseded = supersedeId;
|
|
5288
|
+
} catch (err) {
|
|
5289
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5290
|
+
console.debug(`[harmony_remember] supersede ${supersedeId} failed: ${msg}`);
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
4909
5293
|
const newEntityIdForGraph = result.entity?.id;
|
|
4910
5294
|
if (newEntityIdForGraph) {
|
|
4911
|
-
autoExpandGraph(client3, newEntityIdForGraph, title, content, entityTags, workspaceId,
|
|
5295
|
+
autoExpandGraph(client3, newEntityIdForGraph, title, content, entityTags, workspaceId, projectIdForWrite).catch(() => {});
|
|
4912
5296
|
}
|
|
4913
5297
|
if (activeMemSession) {
|
|
4914
5298
|
appendMemoryAction(activeMemSession.cardId, `Stored memory: ${title}`);
|
|
@@ -4916,7 +5300,10 @@ async function handleToolCall(name, args, deps) {
|
|
|
4916
5300
|
}
|
|
4917
5301
|
return {
|
|
4918
5302
|
success: true,
|
|
4919
|
-
...result
|
|
5303
|
+
...result,
|
|
5304
|
+
...similar.length > 0 ? { similar } : {},
|
|
5305
|
+
...superseded ? { superseded } : {},
|
|
5306
|
+
...tagSuggestions.length > 0 ? { tagSuggestions } : {}
|
|
4920
5307
|
};
|
|
4921
5308
|
}
|
|
4922
5309
|
case "harmony_recall": {
|
|
@@ -4955,9 +5342,7 @@ async function handleToolCall(name, args, deps) {
|
|
|
4955
5342
|
const wanted = new Set(requestedTags);
|
|
4956
5343
|
entities = entities.filter((e) => (e?.tags ?? []).some((t) => wanted.has(t)));
|
|
4957
5344
|
}
|
|
4958
|
-
|
|
4959
|
-
entities = entities.filter((e) => typeof e?.confidence === "number" && e.confidence >= minConfidence);
|
|
4960
|
-
}
|
|
5345
|
+
entities = filterByMinConfidence(entities, minConfidence);
|
|
4961
5346
|
if (!includeSuperseded) {
|
|
4962
5347
|
entities = entities.filter((e) => !e?.superseded_at);
|
|
4963
5348
|
}
|
|
@@ -4988,16 +5373,10 @@ async function handleToolCall(name, args, deps) {
|
|
|
4988
5373
|
trimmed = fitToBudget(trimmed, budgetTokens);
|
|
4989
5374
|
}
|
|
4990
5375
|
if (trimmed.length > 0) {
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
...entity.metadata || {},
|
|
4996
|
-
_last_recall: new Date().toISOString()
|
|
4997
|
-
}
|
|
4998
|
-
});
|
|
4999
|
-
} catch (_) {}
|
|
5000
|
-
})).catch(() => {});
|
|
5376
|
+
const touchIds = trimmed.map(({ entity }) => entity?.id).filter((id) => typeof id === "string");
|
|
5377
|
+
if (touchIds.length > 0) {
|
|
5378
|
+
client3.batchTouchMemoryEntities(touchIds).catch(() => {});
|
|
5379
|
+
}
|
|
5001
5380
|
}
|
|
5002
5381
|
let sessionEntities = [];
|
|
5003
5382
|
if (excludeSessionFromLongTerm && recallMemSession?.agentSessionId) {
|
|
@@ -5064,8 +5443,12 @@ async function handleToolCall(name, args, deps) {
|
|
|
5064
5443
|
updates.type = args.type;
|
|
5065
5444
|
if (args.scope !== undefined)
|
|
5066
5445
|
updates.scope = args.scope;
|
|
5067
|
-
|
|
5068
|
-
|
|
5446
|
+
let updateTagSuggestions = [];
|
|
5447
|
+
if (args.tags !== undefined) {
|
|
5448
|
+
const updateRawTags = args.tags;
|
|
5449
|
+
updates.tags = normalizeTags(updateRawTags);
|
|
5450
|
+
updateTagSuggestions = lintTags(updateRawTags);
|
|
5451
|
+
}
|
|
5069
5452
|
if (args.confidence !== undefined)
|
|
5070
5453
|
updates.confidence = z.number().min(0).max(1).parse(args.confidence);
|
|
5071
5454
|
if (args.metadata !== undefined)
|
|
@@ -5077,7 +5460,11 @@ async function handleToolCall(name, args, deps) {
|
|
|
5077
5460
|
appendMemoryAction(updateMemSession.cardId, `Updated memory: ${updateTitle}`);
|
|
5078
5461
|
flushMemoryActions(client3, updateMemSession.cardId).catch(() => {});
|
|
5079
5462
|
}
|
|
5080
|
-
return {
|
|
5463
|
+
return {
|
|
5464
|
+
success: true,
|
|
5465
|
+
...result,
|
|
5466
|
+
...updateTagSuggestions.length > 0 ? { tagSuggestions: updateTagSuggestions } : {}
|
|
5467
|
+
};
|
|
5081
5468
|
}
|
|
5082
5469
|
case "harmony_forget": {
|
|
5083
5470
|
const entityId = z.string().uuid().parse(args.entityId);
|
|
@@ -5122,6 +5509,64 @@ async function handleToolCall(name, args, deps) {
|
|
|
5122
5509
|
}
|
|
5123
5510
|
return { success: true, ...result };
|
|
5124
5511
|
}
|
|
5512
|
+
case "harmony_suggest_relations": {
|
|
5513
|
+
const entityId = z.string().uuid().parse(args.entityId);
|
|
5514
|
+
const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
|
|
5515
|
+
if (!workspaceId) {
|
|
5516
|
+
throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
|
|
5517
|
+
}
|
|
5518
|
+
const projectId = args.projectId || deps.getActiveProjectId() || undefined;
|
|
5519
|
+
const limit = args.limit ?? 5;
|
|
5520
|
+
const { entity } = await client3.getMemoryEntity(entityId);
|
|
5521
|
+
const src = entity;
|
|
5522
|
+
if (!src) {
|
|
5523
|
+
throw new Error(`Entity not found: ${entityId}`);
|
|
5524
|
+
}
|
|
5525
|
+
const related = await client3.getRelatedEntities(entityId);
|
|
5526
|
+
const excludeIds = new Set([entityId]);
|
|
5527
|
+
for (const raw of [
|
|
5528
|
+
...related.outgoing || [],
|
|
5529
|
+
...related.incoming || []
|
|
5530
|
+
]) {
|
|
5531
|
+
const rel = raw;
|
|
5532
|
+
const target = rel.target;
|
|
5533
|
+
const source = rel.source;
|
|
5534
|
+
const otherId = target?.id ?? rel.target_id ?? source?.id ?? rel.source_id;
|
|
5535
|
+
if (otherId)
|
|
5536
|
+
excludeIds.add(otherId);
|
|
5537
|
+
}
|
|
5538
|
+
const suggestions = await findSimilarEntities(client3, src.title ?? "", src.content ?? "", workspaceId, {
|
|
5539
|
+
projectId,
|
|
5540
|
+
limit: limit + excludeIds.size,
|
|
5541
|
+
excludeIds: [...excludeIds]
|
|
5542
|
+
});
|
|
5543
|
+
return {
|
|
5544
|
+
success: true,
|
|
5545
|
+
suggestions: suggestions.slice(0, limit).map((s) => ({
|
|
5546
|
+
id: s.id,
|
|
5547
|
+
title: s.title,
|
|
5548
|
+
type: s.type,
|
|
5549
|
+
rrf_score: s.rrf_score
|
|
5550
|
+
}))
|
|
5551
|
+
};
|
|
5552
|
+
}
|
|
5553
|
+
case "harmony_recall_feedback": {
|
|
5554
|
+
const entityId = z.string().uuid().parse(args.entityId);
|
|
5555
|
+
const vote = z.enum(["up", "down"]).parse(args.vote);
|
|
5556
|
+
const { entity } = await client3.getMemoryEntity(entityId);
|
|
5557
|
+
const existingMeta = entity?.metadata ?? {};
|
|
5558
|
+
const existingFeedback = existingMeta.feedback;
|
|
5559
|
+
const feedback = mergeFeedback(existingFeedback, vote);
|
|
5560
|
+
const result = await client3.updateMemoryEntity(entityId, {
|
|
5561
|
+
metadata: { ...existingMeta, feedback }
|
|
5562
|
+
});
|
|
5563
|
+
const fbMemSession = getActiveMemorySession();
|
|
5564
|
+
if (fbMemSession) {
|
|
5565
|
+
appendMemoryAction(fbMemSession.cardId, `Memory feedback: ${vote === "up" ? "\uD83D\uDC4D" : "\uD83D\uDC4E"} ${entityId.slice(0, 8)}`);
|
|
5566
|
+
flushMemoryActions(client3, fbMemSession.cardId).catch(() => {});
|
|
5567
|
+
}
|
|
5568
|
+
return { success: true, feedback, ...result };
|
|
5569
|
+
}
|
|
5125
5570
|
case "harmony_memory_search": {
|
|
5126
5571
|
const query = z.string().min(1).max(500).parse(args.query);
|
|
5127
5572
|
const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
|