@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/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} ` : "";
|
|
@@ -1551,6 +1561,9 @@ class HarmonyApiClient {
|
|
|
1551
1561
|
async toggleSubtask(subtaskId) {
|
|
1552
1562
|
return this.request("POST", `/subtasks/${subtaskId}/toggle`);
|
|
1553
1563
|
}
|
|
1564
|
+
async updateSubtask(subtaskId, updates) {
|
|
1565
|
+
return this.request("PATCH", `/subtasks/${subtaskId}`, updates);
|
|
1566
|
+
}
|
|
1554
1567
|
async deleteSubtask(subtaskId) {
|
|
1555
1568
|
return this.request("DELETE", `/subtasks/${subtaskId}`);
|
|
1556
1569
|
}
|
|
@@ -2024,9 +2037,9 @@ function resolveAgentIdentity(info) {
|
|
|
2024
2037
|
}
|
|
2025
2038
|
var AUTO_START_TRIGGERS = new Set([
|
|
2026
2039
|
"harmony_generate_prompt",
|
|
2027
|
-
"harmony_update_card",
|
|
2028
2040
|
"harmony_create_subtask",
|
|
2029
|
-
"harmony_toggle_subtask"
|
|
2041
|
+
"harmony_toggle_subtask",
|
|
2042
|
+
"harmony_update_subtask"
|
|
2030
2043
|
]);
|
|
2031
2044
|
var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
2032
2045
|
var CHECK_INTERVAL_MS = 60 * 1000;
|
|
@@ -2055,6 +2068,10 @@ async function trackActivity(cardId, options) {
|
|
|
2055
2068
|
const client3 = options?.client ?? clientGetter?.();
|
|
2056
2069
|
if (!client3)
|
|
2057
2070
|
return;
|
|
2071
|
+
const info = clientInfoGetter?.() ?? null;
|
|
2072
|
+
if (!info?.name)
|
|
2073
|
+
return;
|
|
2074
|
+
const { agentIdentifier, agentName } = resolveAgentIdentity(info);
|
|
2058
2075
|
const toEnd = [];
|
|
2059
2076
|
for (const [otherCardId, session] of activeSessions) {
|
|
2060
2077
|
if (otherCardId !== cardId && !session.isExplicit) {
|
|
@@ -2064,8 +2081,6 @@ async function trackActivity(cardId, options) {
|
|
|
2064
2081
|
for (const otherCardId of toEnd) {
|
|
2065
2082
|
await autoEndSession(client3, otherCardId, "completed");
|
|
2066
2083
|
}
|
|
2067
|
-
const info = clientInfoGetter?.() ?? null;
|
|
2068
|
-
const { agentIdentifier, agentName } = resolveAgentIdentity(info);
|
|
2069
2084
|
try {
|
|
2070
2085
|
await client3.startAgentSession(cardId, {
|
|
2071
2086
|
agentIdentifier,
|
|
@@ -2185,6 +2200,90 @@ async function autoExpandGraph(client3, entityId, title, content, _tags, workspa
|
|
|
2185
2200
|
return { relationsCreated: 0 };
|
|
2186
2201
|
}
|
|
2187
2202
|
}
|
|
2203
|
+
async function findSimilarEntities(client3, title, content, workspaceId, options) {
|
|
2204
|
+
const contentSnippet = content.slice(0, 200).trim();
|
|
2205
|
+
const query = [title, contentSnippet].filter(Boolean).join(" ");
|
|
2206
|
+
try {
|
|
2207
|
+
const { entities } = await client3.searchMemoryEntities(workspaceId, query, {
|
|
2208
|
+
project_id: options?.projectId,
|
|
2209
|
+
limit: options?.limit ?? 20,
|
|
2210
|
+
type: options?.type
|
|
2211
|
+
});
|
|
2212
|
+
const minScore = options?.minRrfScore ?? 0;
|
|
2213
|
+
const excludeSet = new Set(options?.excludeIds || []);
|
|
2214
|
+
return entities.filter((e) => {
|
|
2215
|
+
if (excludeSet.has(e.id))
|
|
2216
|
+
return false;
|
|
2217
|
+
if (minScore > 0 && (e.rrf_score ?? 0) < minScore)
|
|
2218
|
+
return false;
|
|
2219
|
+
return true;
|
|
2220
|
+
});
|
|
2221
|
+
} catch {
|
|
2222
|
+
return [];
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
var SUPERSEDE_RRF_THRESHOLD = 0.029;
|
|
2226
|
+
var SUPERSEDE_TITLE_OVERLAP = 0.5;
|
|
2227
|
+
var TITLE_STOPWORDS = new Set([
|
|
2228
|
+
"a",
|
|
2229
|
+
"an",
|
|
2230
|
+
"the",
|
|
2231
|
+
"and",
|
|
2232
|
+
"or",
|
|
2233
|
+
"of",
|
|
2234
|
+
"to",
|
|
2235
|
+
"in",
|
|
2236
|
+
"on",
|
|
2237
|
+
"for",
|
|
2238
|
+
"with",
|
|
2239
|
+
"is",
|
|
2240
|
+
"are",
|
|
2241
|
+
"be",
|
|
2242
|
+
"by",
|
|
2243
|
+
"at",
|
|
2244
|
+
"as"
|
|
2245
|
+
]);
|
|
2246
|
+
function significantTitleTokens(title) {
|
|
2247
|
+
return new Set(title.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 2 && !TITLE_STOPWORDS.has(t)));
|
|
2248
|
+
}
|
|
2249
|
+
function jaccard(a, b) {
|
|
2250
|
+
if (a.size === 0 || b.size === 0)
|
|
2251
|
+
return 0;
|
|
2252
|
+
let intersection = 0;
|
|
2253
|
+
for (const t of a)
|
|
2254
|
+
if (b.has(t))
|
|
2255
|
+
intersection++;
|
|
2256
|
+
return intersection / (a.size + b.size - intersection);
|
|
2257
|
+
}
|
|
2258
|
+
async function findSupersedeCandidates(client3, title, content, type, workspaceId, options) {
|
|
2259
|
+
const rrfThreshold = options?.rrfThreshold ?? SUPERSEDE_RRF_THRESHOLD;
|
|
2260
|
+
const titleOverlap = options?.titleOverlap ?? SUPERSEDE_TITLE_OVERLAP;
|
|
2261
|
+
const candidateTokens = significantTitleTokens(title);
|
|
2262
|
+
try {
|
|
2263
|
+
const hits = await findSimilarEntities(client3, title, content, workspaceId, {
|
|
2264
|
+
projectId: options?.projectId,
|
|
2265
|
+
type,
|
|
2266
|
+
limit: options?.limit ?? 10,
|
|
2267
|
+
minRrfScore: rrfThreshold
|
|
2268
|
+
});
|
|
2269
|
+
return hits.filter((e) => {
|
|
2270
|
+
if (options?.scope && e.scope !== undefined) {
|
|
2271
|
+
if (e.scope !== options.scope)
|
|
2272
|
+
return false;
|
|
2273
|
+
}
|
|
2274
|
+
if (e.superseded_at) {
|
|
2275
|
+
return false;
|
|
2276
|
+
}
|
|
2277
|
+
return jaccard(candidateTokens, significantTitleTokens(e.title)) >= titleOverlap;
|
|
2278
|
+
}).map((e) => ({
|
|
2279
|
+
id: e.id,
|
|
2280
|
+
title: e.title,
|
|
2281
|
+
score: e.rrf_score ?? 0
|
|
2282
|
+
}));
|
|
2283
|
+
} catch {
|
|
2284
|
+
return [];
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2188
2287
|
|
|
2189
2288
|
// src/memory-floor.ts
|
|
2190
2289
|
var STOP_WORDS = new Set([
|
|
@@ -2323,6 +2422,10 @@ var DEFAULT_WEIGHTS = {
|
|
|
2323
2422
|
recency: 0.25,
|
|
2324
2423
|
importance: 0.2
|
|
2325
2424
|
};
|
|
2425
|
+
var USAGE_BUMP_SCALE = 0.6;
|
|
2426
|
+
var USAGE_BUMP_MAX = 2;
|
|
2427
|
+
var FEEDBACK_BUMP_SCALE = 0.8;
|
|
2428
|
+
var FEEDBACK_BUMP_MAX = 2;
|
|
2326
2429
|
var TYPE_TAU_SECONDS = {
|
|
2327
2430
|
preference: Number.POSITIVE_INFINITY,
|
|
2328
2431
|
pattern: 60 * 60 * 24 * 180,
|
|
@@ -2378,8 +2481,37 @@ function recencyDecay(lastAccessedAt, createdAt, type, now) {
|
|
|
2378
2481
|
const dtSec = Math.max(0, (now.getTime() - t) / 1000);
|
|
2379
2482
|
return clamp01(Math.exp(-dtSec / tau));
|
|
2380
2483
|
}
|
|
2381
|
-
function
|
|
2484
|
+
function baseImportance(raw, type) {
|
|
2382
2485
|
let v = typeof raw === "number" ? raw : TYPE_IMPORTANCE_DEFAULT[type] ?? 5;
|
|
2486
|
+
if (v < 1)
|
|
2487
|
+
v = 1;
|
|
2488
|
+
if (v > 10)
|
|
2489
|
+
v = 10;
|
|
2490
|
+
return v;
|
|
2491
|
+
}
|
|
2492
|
+
function usageBump(accessCount) {
|
|
2493
|
+
const n = typeof accessCount === "number" && accessCount > 0 ? accessCount : 0;
|
|
2494
|
+
if (n === 0)
|
|
2495
|
+
return 0;
|
|
2496
|
+
return Math.min(USAGE_BUMP_MAX, USAGE_BUMP_SCALE * Math.log(1 + n));
|
|
2497
|
+
}
|
|
2498
|
+
function feedbackBump(feedback) {
|
|
2499
|
+
const up = typeof feedback?.up === "number" ? feedback.up : 0;
|
|
2500
|
+
const down = typeof feedback?.down === "number" ? feedback.down : 0;
|
|
2501
|
+
const net = up - down;
|
|
2502
|
+
if (net === 0)
|
|
2503
|
+
return 0;
|
|
2504
|
+
const raw = FEEDBACK_BUMP_SCALE * Math.sign(net) * Math.log(1 + Math.abs(net));
|
|
2505
|
+
if (raw > FEEDBACK_BUMP_MAX)
|
|
2506
|
+
return FEEDBACK_BUMP_MAX;
|
|
2507
|
+
if (raw < -FEEDBACK_BUMP_MAX)
|
|
2508
|
+
return -FEEDBACK_BUMP_MAX;
|
|
2509
|
+
return raw;
|
|
2510
|
+
}
|
|
2511
|
+
function effectiveImportance(entity) {
|
|
2512
|
+
const base = baseImportance(entity.importance, entity.type);
|
|
2513
|
+
const bump = usageBump(entity.access_count) + feedbackBump(entity.metadata?.feedback);
|
|
2514
|
+
let v = base + bump;
|
|
2383
2515
|
if (v < 1)
|
|
2384
2516
|
v = 1;
|
|
2385
2517
|
if (v > 10)
|
|
@@ -2393,7 +2525,7 @@ function rescore(entities, options = {}) {
|
|
|
2393
2525
|
const scored = entities.map((entity) => {
|
|
2394
2526
|
const relevance = clamp01(relevanceMap.get(entity.id ?? "") ?? 0.5);
|
|
2395
2527
|
const recency = recencyDecay(entity.last_accessed_at, entity.created_at, entity.type, now);
|
|
2396
|
-
const importance =
|
|
2528
|
+
const importance = effectiveImportance(entity);
|
|
2397
2529
|
const score = w.relevance * relevance + w.recency * recency + w.importance * importance;
|
|
2398
2530
|
return { entity, relevance, recency, importance, score };
|
|
2399
2531
|
});
|
|
@@ -2406,6 +2538,11 @@ function rescore(entities, options = {}) {
|
|
|
2406
2538
|
});
|
|
2407
2539
|
return scored;
|
|
2408
2540
|
}
|
|
2541
|
+
function filterByMinConfidence(entities, minConfidence) {
|
|
2542
|
+
if (typeof minConfidence !== "number")
|
|
2543
|
+
return entities;
|
|
2544
|
+
return entities.filter((e) => typeof e.confidence === "number" && e.confidence >= minConfidence);
|
|
2545
|
+
}
|
|
2409
2546
|
function relevanceFromRank(ranked, decay = 10) {
|
|
2410
2547
|
const out = new Map;
|
|
2411
2548
|
ranked.forEach((entity, rank) => {
|
|
@@ -2438,6 +2575,82 @@ function fitToBudget(scored, budgetTokens) {
|
|
|
2438
2575
|
}
|
|
2439
2576
|
return out;
|
|
2440
2577
|
}
|
|
2578
|
+
function mergeFeedback(existing, vote) {
|
|
2579
|
+
const up = typeof existing?.up === "number" && existing.up > 0 ? existing.up : 0;
|
|
2580
|
+
const down = typeof existing?.down === "number" && existing.down > 0 ? existing.down : 0;
|
|
2581
|
+
return vote === "up" ? { up: up + 1, down } : { up, down: down + 1 };
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// src/memory-provenance.ts
|
|
2585
|
+
function buildOrigin(input) {
|
|
2586
|
+
const origin = { source: input.source };
|
|
2587
|
+
if (input.source_card_id)
|
|
2588
|
+
origin.source_card_id = input.source_card_id;
|
|
2589
|
+
if (input.source_session_id)
|
|
2590
|
+
origin.source_session_id = input.source_session_id;
|
|
2591
|
+
if (input.author)
|
|
2592
|
+
origin.author = input.author;
|
|
2593
|
+
if (input.source_trust)
|
|
2594
|
+
origin.source_trust = input.source_trust;
|
|
2595
|
+
return origin;
|
|
2596
|
+
}
|
|
2597
|
+
function defaultConfidenceForSource(input) {
|
|
2598
|
+
if (input.source_trust === "document")
|
|
2599
|
+
return 0.9;
|
|
2600
|
+
if (input.source_trust === "manual")
|
|
2601
|
+
return 0.9;
|
|
2602
|
+
switch (input.source) {
|
|
2603
|
+
case "manual":
|
|
2604
|
+
return 0.9;
|
|
2605
|
+
case "assistant":
|
|
2606
|
+
return 0.8;
|
|
2607
|
+
case "agent-run":
|
|
2608
|
+
return 0.6;
|
|
2609
|
+
case "import":
|
|
2610
|
+
return 0.6;
|
|
2611
|
+
default:
|
|
2612
|
+
return 0.7;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
var TYPE_IMPORTANCE_DEFAULT2 = {
|
|
2616
|
+
preference: 9,
|
|
2617
|
+
lesson: 8,
|
|
2618
|
+
decision: 8,
|
|
2619
|
+
pattern: 7,
|
|
2620
|
+
solution: 7,
|
|
2621
|
+
procedure: 7,
|
|
2622
|
+
error: 5,
|
|
2623
|
+
context: 5,
|
|
2624
|
+
task: 5,
|
|
2625
|
+
agent: 5,
|
|
2626
|
+
relationship: 6,
|
|
2627
|
+
commitment: 7,
|
|
2628
|
+
project: 6,
|
|
2629
|
+
handoff: 6
|
|
2630
|
+
};
|
|
2631
|
+
var FILE_PATH = /(?:[\w.-]+\/){1,}[\w.-]+\.[a-z]{1,4}\b|(?:[\w-]+\/){2,}[\w-]+/i;
|
|
2632
|
+
var GUIDANCE_SECTION = /\b(why|how to apply|how to use|takeaway|root cause)\b/i;
|
|
2633
|
+
var PROPER_NOUN = /`[^`]+`|\b[A-Z][a-z0-9]+[A-Z][A-Za-z0-9]*\b|\b[a-z]+_[a-z][\w]*\b/;
|
|
2634
|
+
function signalDensity(content) {
|
|
2635
|
+
let n = 0;
|
|
2636
|
+
if (FILE_PATH.test(content))
|
|
2637
|
+
n += 1;
|
|
2638
|
+
if (GUIDANCE_SECTION.test(content))
|
|
2639
|
+
n += 1;
|
|
2640
|
+
if (PROPER_NOUN.test(content))
|
|
2641
|
+
n += 1;
|
|
2642
|
+
return n;
|
|
2643
|
+
}
|
|
2644
|
+
function importanceWithSignalBump(type, content) {
|
|
2645
|
+
const base = TYPE_IMPORTANCE_DEFAULT2[type] ?? 5;
|
|
2646
|
+
const bump = Math.min(2, signalDensity(content));
|
|
2647
|
+
const v = base + bump;
|
|
2648
|
+
if (v < 1)
|
|
2649
|
+
return 1;
|
|
2650
|
+
if (v > 10)
|
|
2651
|
+
return 10;
|
|
2652
|
+
return v;
|
|
2653
|
+
}
|
|
2441
2654
|
|
|
2442
2655
|
// src/memory-session.ts
|
|
2443
2656
|
var SESSION_SCOPE_PREFIX = "session:";
|
|
@@ -2458,6 +2671,49 @@ function sessionScopeFor(agentSessionId) {
|
|
|
2458
2671
|
return `${SESSION_SCOPE_PREFIX}${agentSessionId}`;
|
|
2459
2672
|
}
|
|
2460
2673
|
|
|
2674
|
+
// src/memory-tags.ts
|
|
2675
|
+
var CARD_REF = /^card[\s:#-]+(\d+)$/;
|
|
2676
|
+
function normalizeTag(tag) {
|
|
2677
|
+
const cleaned = tag.trim().toLowerCase().replace(/\s+/g, " ");
|
|
2678
|
+
const cardMatch = cleaned.match(CARD_REF);
|
|
2679
|
+
if (cardMatch) {
|
|
2680
|
+
return `card:${cardMatch[1]}`;
|
|
2681
|
+
}
|
|
2682
|
+
return cleaned;
|
|
2683
|
+
}
|
|
2684
|
+
function normalizeTags(tags) {
|
|
2685
|
+
const seen = new Set;
|
|
2686
|
+
const out = [];
|
|
2687
|
+
for (const raw of tags) {
|
|
2688
|
+
const norm = normalizeTag(raw);
|
|
2689
|
+
if (!norm || seen.has(norm))
|
|
2690
|
+
continue;
|
|
2691
|
+
seen.add(norm);
|
|
2692
|
+
out.push(norm);
|
|
2693
|
+
}
|
|
2694
|
+
return out;
|
|
2695
|
+
}
|
|
2696
|
+
function lintTags(tags) {
|
|
2697
|
+
const suggestions = [];
|
|
2698
|
+
for (const tag of tags) {
|
|
2699
|
+
const norm = normalizeTag(tag);
|
|
2700
|
+
if (norm === tag)
|
|
2701
|
+
continue;
|
|
2702
|
+
let reason;
|
|
2703
|
+
if (CARD_REF.test(tag.trim().toLowerCase()) && norm.startsWith("card:")) {
|
|
2704
|
+
reason = "non-canonical card ref; use 'card:<n>'";
|
|
2705
|
+
} else if (tag !== tag.trim() || /\s{2,}/.test(tag)) {
|
|
2706
|
+
reason = "whitespace; trim and collapse";
|
|
2707
|
+
} else if (tag !== tag.toLowerCase()) {
|
|
2708
|
+
reason = "case variant; use lowercase";
|
|
2709
|
+
} else {
|
|
2710
|
+
reason = "normalize to canonical form";
|
|
2711
|
+
}
|
|
2712
|
+
suggestions.push({ tag, suggestion: norm, reason });
|
|
2713
|
+
}
|
|
2714
|
+
return suggestions;
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2461
2717
|
// src/onboard.ts
|
|
2462
2718
|
async function onboardNewUser(params) {
|
|
2463
2719
|
const {
|
|
@@ -2944,7 +3200,12 @@ var TOOLS = {
|
|
|
2944
3200
|
priority: { type: "string", enum: ["low", "medium", "high", "urgent"] },
|
|
2945
3201
|
assigneeId: { type: "string", nullable: true },
|
|
2946
3202
|
dueDate: { type: "string", nullable: true },
|
|
2947
|
-
done: { type: "boolean", description: "Mark card as done or not done" }
|
|
3203
|
+
done: { type: "boolean", description: "Mark card as done or not done" },
|
|
3204
|
+
planId: {
|
|
3205
|
+
type: "string",
|
|
3206
|
+
nullable: true,
|
|
3207
|
+
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."
|
|
3208
|
+
}
|
|
2948
3209
|
},
|
|
2949
3210
|
required: ["cardId"]
|
|
2950
3211
|
}
|
|
@@ -3243,6 +3504,25 @@ var TOOLS = {
|
|
|
3243
3504
|
required: ["subtaskId"]
|
|
3244
3505
|
}
|
|
3245
3506
|
},
|
|
3507
|
+
harmony_update_subtask: {
|
|
3508
|
+
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.",
|
|
3509
|
+
inputSchema: {
|
|
3510
|
+
type: "object",
|
|
3511
|
+
properties: {
|
|
3512
|
+
subtaskId: { type: "string" },
|
|
3513
|
+
title: { type: "string", description: "New title (1–500 chars)" },
|
|
3514
|
+
completed: {
|
|
3515
|
+
type: "boolean",
|
|
3516
|
+
description: "Explicit completion state (set, not toggle)"
|
|
3517
|
+
},
|
|
3518
|
+
position: {
|
|
3519
|
+
type: "number",
|
|
3520
|
+
description: "New position (0-based ordering within the card)"
|
|
3521
|
+
}
|
|
3522
|
+
},
|
|
3523
|
+
required: ["subtaskId"]
|
|
3524
|
+
}
|
|
3525
|
+
},
|
|
3246
3526
|
harmony_delete_subtask: {
|
|
3247
3527
|
description: "Delete a subtask",
|
|
3248
3528
|
inputSchema: {
|
|
@@ -3651,6 +3931,10 @@ var TOOLS = {
|
|
|
3651
3931
|
projectId: {
|
|
3652
3932
|
type: "string",
|
|
3653
3933
|
description: "Project ID (optional, required if scope is 'project')"
|
|
3934
|
+
},
|
|
3935
|
+
supersedeId: {
|
|
3936
|
+
type: "string",
|
|
3937
|
+
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."
|
|
3654
3938
|
}
|
|
3655
3939
|
},
|
|
3656
3940
|
required: ["title", "content"]
|
|
@@ -3824,6 +4108,49 @@ var TOOLS = {
|
|
|
3824
4108
|
required: ["sourceId", "targetId", "relationType"]
|
|
3825
4109
|
}
|
|
3826
4110
|
},
|
|
4111
|
+
harmony_suggest_relations: {
|
|
4112
|
+
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.",
|
|
4113
|
+
inputSchema: {
|
|
4114
|
+
type: "object",
|
|
4115
|
+
properties: {
|
|
4116
|
+
entityId: {
|
|
4117
|
+
type: "string",
|
|
4118
|
+
description: "The entity UUID to find related candidates for"
|
|
4119
|
+
},
|
|
4120
|
+
limit: {
|
|
4121
|
+
type: "number",
|
|
4122
|
+
description: "Max suggestions to return (default: 5)"
|
|
4123
|
+
},
|
|
4124
|
+
workspaceId: {
|
|
4125
|
+
type: "string",
|
|
4126
|
+
description: "Workspace ID (optional if context set)"
|
|
4127
|
+
},
|
|
4128
|
+
projectId: {
|
|
4129
|
+
type: "string",
|
|
4130
|
+
description: "Project ID (optional)"
|
|
4131
|
+
}
|
|
4132
|
+
},
|
|
4133
|
+
required: ["entityId"]
|
|
4134
|
+
}
|
|
4135
|
+
},
|
|
4136
|
+
harmony_recall_feedback: {
|
|
4137
|
+
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').",
|
|
4138
|
+
inputSchema: {
|
|
4139
|
+
type: "object",
|
|
4140
|
+
properties: {
|
|
4141
|
+
entityId: {
|
|
4142
|
+
type: "string",
|
|
4143
|
+
description: "Memory entity UUID that was recalled"
|
|
4144
|
+
},
|
|
4145
|
+
vote: {
|
|
4146
|
+
type: "string",
|
|
4147
|
+
enum: ["up", "down"],
|
|
4148
|
+
description: "'up' if the memory was useful, 'down' if it was misleading or irrelevant"
|
|
4149
|
+
}
|
|
4150
|
+
},
|
|
4151
|
+
required: ["entityId", "vote"]
|
|
4152
|
+
}
|
|
4153
|
+
},
|
|
3827
4154
|
harmony_memory_search: {
|
|
3828
4155
|
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.",
|
|
3829
4156
|
inputSchema: {
|
|
@@ -4360,13 +4687,15 @@ async function handleToolCall(name, args, deps) {
|
|
|
4360
4687
|
}
|
|
4361
4688
|
case "harmony_update_card": {
|
|
4362
4689
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
4690
|
+
const planId = args.planId === undefined ? undefined : args.planId === null ? null : z.string().uuid().parse(args.planId);
|
|
4363
4691
|
const result = await client3.updateCard(cardId, {
|
|
4364
4692
|
title: args.title,
|
|
4365
4693
|
description: args.description,
|
|
4366
4694
|
priority: args.priority,
|
|
4367
4695
|
assigneeId: args.assigneeId,
|
|
4368
4696
|
dueDate: args.dueDate,
|
|
4369
|
-
done: args.done
|
|
4697
|
+
done: args.done,
|
|
4698
|
+
planId
|
|
4370
4699
|
});
|
|
4371
4700
|
return { success: true, ...result };
|
|
4372
4701
|
}
|
|
@@ -4569,6 +4898,24 @@ async function handleToolCall(name, args, deps) {
|
|
|
4569
4898
|
const result = await client3.toggleSubtask(subtaskId);
|
|
4570
4899
|
return { success: true, ...result };
|
|
4571
4900
|
}
|
|
4901
|
+
case "harmony_update_subtask": {
|
|
4902
|
+
const subtaskId = z.string().uuid().parse(args.subtaskId);
|
|
4903
|
+
const updates = {};
|
|
4904
|
+
if (args.title !== undefined) {
|
|
4905
|
+
updates.title = z.string().min(1).max(500).parse(args.title);
|
|
4906
|
+
}
|
|
4907
|
+
if (args.completed !== undefined) {
|
|
4908
|
+
updates.completed = z.boolean().parse(args.completed);
|
|
4909
|
+
}
|
|
4910
|
+
if (args.position !== undefined) {
|
|
4911
|
+
updates.position = z.number().int().min(0).parse(args.position);
|
|
4912
|
+
}
|
|
4913
|
+
if (Object.keys(updates).length === 0) {
|
|
4914
|
+
throw new Error("harmony_update_subtask requires at least one of: title, completed, position");
|
|
4915
|
+
}
|
|
4916
|
+
const result = await client3.updateSubtask(subtaskId, updates);
|
|
4917
|
+
return { success: true, ...result };
|
|
4918
|
+
}
|
|
4572
4919
|
case "harmony_delete_subtask": {
|
|
4573
4920
|
const subtaskId = z.string().uuid().parse(args.subtaskId);
|
|
4574
4921
|
await client3.deleteSubtask(subtaskId);
|
|
@@ -4881,7 +5228,9 @@ async function handleToolCall(name, args, deps) {
|
|
|
4881
5228
|
throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
|
|
4882
5229
|
}
|
|
4883
5230
|
const entityType = args.type || "context";
|
|
4884
|
-
const
|
|
5231
|
+
const rawTags = args.tags || [];
|
|
5232
|
+
const entityTags = normalizeTags(rawTags);
|
|
5233
|
+
const tagSuggestions = lintTags(rawTags);
|
|
4885
5234
|
const activeMemSession = getActiveMemorySession();
|
|
4886
5235
|
const entityScope = resolveSessionScope(args.scope, activeMemSession?.agentSessionId) || "project";
|
|
4887
5236
|
const floorRejection = validateMemoryQuality({
|
|
@@ -4895,24 +5244,59 @@ async function handleToolCall(name, args, deps) {
|
|
|
4895
5244
|
if (floorRejection) {
|
|
4896
5245
|
throw new Error(`Memory rejected by Utility Floor [${floorRejection.rule}]: ${floorRejection.message}`);
|
|
4897
5246
|
}
|
|
4898
|
-
const
|
|
5247
|
+
const sourceTrust = args.source_trust;
|
|
5248
|
+
const source = activeMemSession ? activeMemSession.agentIdentifier === "assistant" ? "assistant" : "agent-run" : "manual";
|
|
5249
|
+
const origin = buildOrigin({
|
|
5250
|
+
source,
|
|
5251
|
+
source_card_id: activeMemSession?.cardId,
|
|
5252
|
+
source_session_id: activeMemSession?.agentSessionId,
|
|
5253
|
+
author: activeMemSession?.agentIdentifier ?? "user",
|
|
5254
|
+
source_trust: sourceTrust
|
|
5255
|
+
});
|
|
5256
|
+
const callerMetadata = args.metadata;
|
|
5257
|
+
const metadataWithOrigin = {
|
|
5258
|
+
...callerMetadata ?? {},
|
|
5259
|
+
origin
|
|
5260
|
+
};
|
|
5261
|
+
const confidence = args.confidence !== undefined ? z.number().min(0).max(1).parse(args.confidence) : defaultConfidenceForSource({ source, source_trust: sourceTrust });
|
|
5262
|
+
const importance = args.importance !== undefined ? z.number().int().min(1).max(10).parse(args.importance) : importanceWithSignalBump(entityType, content);
|
|
5263
|
+
const projectIdForWrite = args.projectId || deps.getActiveProjectId() || undefined;
|
|
5264
|
+
let similar = [];
|
|
5265
|
+
if (!isSessionScope(entityScope)) {
|
|
5266
|
+
similar = await findSupersedeCandidates(client3, title, content, entityType, workspaceId, { projectId: projectIdForWrite, scope: entityScope });
|
|
5267
|
+
}
|
|
4899
5268
|
const result = await client3.createMemoryEntity({
|
|
4900
5269
|
workspace_id: workspaceId,
|
|
4901
|
-
project_id:
|
|
5270
|
+
project_id: projectIdForWrite,
|
|
4902
5271
|
type: entityType,
|
|
4903
5272
|
scope: entityScope,
|
|
4904
5273
|
memory_tier: args.tier || undefined,
|
|
4905
5274
|
title,
|
|
4906
5275
|
content,
|
|
4907
|
-
metadata:
|
|
4908
|
-
confidence
|
|
4909
|
-
importance
|
|
5276
|
+
metadata: metadataWithOrigin,
|
|
5277
|
+
confidence,
|
|
5278
|
+
importance,
|
|
4910
5279
|
tags: entityTags.length > 0 ? entityTags : undefined,
|
|
4911
5280
|
agent_identifier: activeMemSession?.agentIdentifier || undefined
|
|
4912
5281
|
});
|
|
5282
|
+
const supersedeId = args.supersedeId;
|
|
5283
|
+
const newEntityId = result.entity?.id;
|
|
5284
|
+
let superseded;
|
|
5285
|
+
if (supersedeId && newEntityId) {
|
|
5286
|
+
try {
|
|
5287
|
+
await client3.updateMemoryEntity(supersedeId, {
|
|
5288
|
+
superseded_by: newEntityId,
|
|
5289
|
+
superseded_at: new Date().toISOString()
|
|
5290
|
+
});
|
|
5291
|
+
superseded = supersedeId;
|
|
5292
|
+
} catch (err) {
|
|
5293
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5294
|
+
console.debug(`[harmony_remember] supersede ${supersedeId} failed: ${msg}`);
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
4913
5297
|
const newEntityIdForGraph = result.entity?.id;
|
|
4914
5298
|
if (newEntityIdForGraph) {
|
|
4915
|
-
autoExpandGraph(client3, newEntityIdForGraph, title, content, entityTags, workspaceId,
|
|
5299
|
+
autoExpandGraph(client3, newEntityIdForGraph, title, content, entityTags, workspaceId, projectIdForWrite).catch(() => {});
|
|
4916
5300
|
}
|
|
4917
5301
|
if (activeMemSession) {
|
|
4918
5302
|
appendMemoryAction(activeMemSession.cardId, `Stored memory: ${title}`);
|
|
@@ -4920,7 +5304,10 @@ async function handleToolCall(name, args, deps) {
|
|
|
4920
5304
|
}
|
|
4921
5305
|
return {
|
|
4922
5306
|
success: true,
|
|
4923
|
-
...result
|
|
5307
|
+
...result,
|
|
5308
|
+
...similar.length > 0 ? { similar } : {},
|
|
5309
|
+
...superseded ? { superseded } : {},
|
|
5310
|
+
...tagSuggestions.length > 0 ? { tagSuggestions } : {}
|
|
4924
5311
|
};
|
|
4925
5312
|
}
|
|
4926
5313
|
case "harmony_recall": {
|
|
@@ -4959,9 +5346,7 @@ async function handleToolCall(name, args, deps) {
|
|
|
4959
5346
|
const wanted = new Set(requestedTags);
|
|
4960
5347
|
entities = entities.filter((e) => (e?.tags ?? []).some((t) => wanted.has(t)));
|
|
4961
5348
|
}
|
|
4962
|
-
|
|
4963
|
-
entities = entities.filter((e) => typeof e?.confidence === "number" && e.confidence >= minConfidence);
|
|
4964
|
-
}
|
|
5349
|
+
entities = filterByMinConfidence(entities, minConfidence);
|
|
4965
5350
|
if (!includeSuperseded) {
|
|
4966
5351
|
entities = entities.filter((e) => !e?.superseded_at);
|
|
4967
5352
|
}
|
|
@@ -4992,16 +5377,10 @@ async function handleToolCall(name, args, deps) {
|
|
|
4992
5377
|
trimmed = fitToBudget(trimmed, budgetTokens);
|
|
4993
5378
|
}
|
|
4994
5379
|
if (trimmed.length > 0) {
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
...entity.metadata || {},
|
|
5000
|
-
_last_recall: new Date().toISOString()
|
|
5001
|
-
}
|
|
5002
|
-
});
|
|
5003
|
-
} catch (_) {}
|
|
5004
|
-
})).catch(() => {});
|
|
5380
|
+
const touchIds = trimmed.map(({ entity }) => entity?.id).filter((id) => typeof id === "string");
|
|
5381
|
+
if (touchIds.length > 0) {
|
|
5382
|
+
client3.batchTouchMemoryEntities(touchIds).catch(() => {});
|
|
5383
|
+
}
|
|
5005
5384
|
}
|
|
5006
5385
|
let sessionEntities = [];
|
|
5007
5386
|
if (excludeSessionFromLongTerm && recallMemSession?.agentSessionId) {
|
|
@@ -5068,8 +5447,12 @@ async function handleToolCall(name, args, deps) {
|
|
|
5068
5447
|
updates.type = args.type;
|
|
5069
5448
|
if (args.scope !== undefined)
|
|
5070
5449
|
updates.scope = args.scope;
|
|
5071
|
-
|
|
5072
|
-
|
|
5450
|
+
let updateTagSuggestions = [];
|
|
5451
|
+
if (args.tags !== undefined) {
|
|
5452
|
+
const updateRawTags = args.tags;
|
|
5453
|
+
updates.tags = normalizeTags(updateRawTags);
|
|
5454
|
+
updateTagSuggestions = lintTags(updateRawTags);
|
|
5455
|
+
}
|
|
5073
5456
|
if (args.confidence !== undefined)
|
|
5074
5457
|
updates.confidence = z.number().min(0).max(1).parse(args.confidence);
|
|
5075
5458
|
if (args.metadata !== undefined)
|
|
@@ -5081,7 +5464,11 @@ async function handleToolCall(name, args, deps) {
|
|
|
5081
5464
|
appendMemoryAction(updateMemSession.cardId, `Updated memory: ${updateTitle}`);
|
|
5082
5465
|
flushMemoryActions(client3, updateMemSession.cardId).catch(() => {});
|
|
5083
5466
|
}
|
|
5084
|
-
return {
|
|
5467
|
+
return {
|
|
5468
|
+
success: true,
|
|
5469
|
+
...result,
|
|
5470
|
+
...updateTagSuggestions.length > 0 ? { tagSuggestions: updateTagSuggestions } : {}
|
|
5471
|
+
};
|
|
5085
5472
|
}
|
|
5086
5473
|
case "harmony_forget": {
|
|
5087
5474
|
const entityId = z.string().uuid().parse(args.entityId);
|
|
@@ -5126,6 +5513,64 @@ async function handleToolCall(name, args, deps) {
|
|
|
5126
5513
|
}
|
|
5127
5514
|
return { success: true, ...result };
|
|
5128
5515
|
}
|
|
5516
|
+
case "harmony_suggest_relations": {
|
|
5517
|
+
const entityId = z.string().uuid().parse(args.entityId);
|
|
5518
|
+
const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
|
|
5519
|
+
if (!workspaceId) {
|
|
5520
|
+
throw new Error("No workspace specified. Use harmony_set_workspace_context or provide workspaceId.");
|
|
5521
|
+
}
|
|
5522
|
+
const projectId = args.projectId || deps.getActiveProjectId() || undefined;
|
|
5523
|
+
const limit = args.limit ?? 5;
|
|
5524
|
+
const { entity } = await client3.getMemoryEntity(entityId);
|
|
5525
|
+
const src = entity;
|
|
5526
|
+
if (!src) {
|
|
5527
|
+
throw new Error(`Entity not found: ${entityId}`);
|
|
5528
|
+
}
|
|
5529
|
+
const related = await client3.getRelatedEntities(entityId);
|
|
5530
|
+
const excludeIds = new Set([entityId]);
|
|
5531
|
+
for (const raw of [
|
|
5532
|
+
...related.outgoing || [],
|
|
5533
|
+
...related.incoming || []
|
|
5534
|
+
]) {
|
|
5535
|
+
const rel = raw;
|
|
5536
|
+
const target = rel.target;
|
|
5537
|
+
const source = rel.source;
|
|
5538
|
+
const otherId = target?.id ?? rel.target_id ?? source?.id ?? rel.source_id;
|
|
5539
|
+
if (otherId)
|
|
5540
|
+
excludeIds.add(otherId);
|
|
5541
|
+
}
|
|
5542
|
+
const suggestions = await findSimilarEntities(client3, src.title ?? "", src.content ?? "", workspaceId, {
|
|
5543
|
+
projectId,
|
|
5544
|
+
limit: limit + excludeIds.size,
|
|
5545
|
+
excludeIds: [...excludeIds]
|
|
5546
|
+
});
|
|
5547
|
+
return {
|
|
5548
|
+
success: true,
|
|
5549
|
+
suggestions: suggestions.slice(0, limit).map((s) => ({
|
|
5550
|
+
id: s.id,
|
|
5551
|
+
title: s.title,
|
|
5552
|
+
type: s.type,
|
|
5553
|
+
rrf_score: s.rrf_score
|
|
5554
|
+
}))
|
|
5555
|
+
};
|
|
5556
|
+
}
|
|
5557
|
+
case "harmony_recall_feedback": {
|
|
5558
|
+
const entityId = z.string().uuid().parse(args.entityId);
|
|
5559
|
+
const vote = z.enum(["up", "down"]).parse(args.vote);
|
|
5560
|
+
const { entity } = await client3.getMemoryEntity(entityId);
|
|
5561
|
+
const existingMeta = entity?.metadata ?? {};
|
|
5562
|
+
const existingFeedback = existingMeta.feedback;
|
|
5563
|
+
const feedback = mergeFeedback(existingFeedback, vote);
|
|
5564
|
+
const result = await client3.updateMemoryEntity(entityId, {
|
|
5565
|
+
metadata: { ...existingMeta, feedback }
|
|
5566
|
+
});
|
|
5567
|
+
const fbMemSession = getActiveMemorySession();
|
|
5568
|
+
if (fbMemSession) {
|
|
5569
|
+
appendMemoryAction(fbMemSession.cardId, `Memory feedback: ${vote === "up" ? "\uD83D\uDC4D" : "\uD83D\uDC4E"} ${entityId.slice(0, 8)}`);
|
|
5570
|
+
flushMemoryActions(client3, fbMemSession.cardId).catch(() => {});
|
|
5571
|
+
}
|
|
5572
|
+
return { success: true, feedback, ...result };
|
|
5573
|
+
}
|
|
5129
5574
|
case "harmony_memory_search": {
|
|
5130
5575
|
const query = z.string().min(1).max(500).parse(args.query);
|
|
5131
5576
|
const workspaceId = args.workspaceId || deps.getActiveWorkspaceId();
|