tuna-agent 0.1.160 → 0.1.162
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.
|
@@ -74,6 +74,8 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
74
74
|
private static extractKeyPhrases;
|
|
75
75
|
/** Check if two rules are semantically similar (>40% key phrase overlap). */
|
|
76
76
|
private static isSimilarRule;
|
|
77
|
+
/** Cosine similarity between two equal-length embedding vectors. */
|
|
78
|
+
private static cosine;
|
|
77
79
|
runSelfImprovement(cwd: string, agentId: string): Promise<void>;
|
|
78
80
|
/**
|
|
79
81
|
* Parse "## Learned Rules" section from CLAUDE.md and store in learnedRulesMap.
|
|
@@ -924,6 +924,16 @@ export class ClaudeCodeAdapter {
|
|
|
924
924
|
const overlap = phrasesA.filter(p => phrasesB.has(p)).length;
|
|
925
925
|
return overlap / phrasesA.length > 0.4;
|
|
926
926
|
}
|
|
927
|
+
/** Cosine similarity between two equal-length embedding vectors. */
|
|
928
|
+
static cosine(a, b) {
|
|
929
|
+
let dot = 0, na = 0, nb = 0;
|
|
930
|
+
for (let i = 0; i < a.length; i++) {
|
|
931
|
+
dot += a[i] * b[i];
|
|
932
|
+
na += a[i] * a[i];
|
|
933
|
+
nb += b[i] * b[i];
|
|
934
|
+
}
|
|
935
|
+
return (na && nb) ? dot / (Math.sqrt(na) * Math.sqrt(nb)) : 0;
|
|
936
|
+
}
|
|
927
937
|
async runSelfImprovement(cwd, agentId) {
|
|
928
938
|
if (!process.env.MEM0_SSH_HOST)
|
|
929
939
|
return;
|
|
@@ -1057,8 +1067,48 @@ export class ClaudeCodeAdapter {
|
|
|
1057
1067
|
console.log(`[Self-Improve] ${patterns.length} patterns found but all similar to existing rules`);
|
|
1058
1068
|
return;
|
|
1059
1069
|
}
|
|
1070
|
+
// ACE-style semantic de-duplication (grow-and-refine): string matching misses
|
|
1071
|
+
// reworded variants (e.g. "prioritize X.com over Threads ..." x11). Embed candidates
|
|
1072
|
+
// + existing rules with mxbai-embed-large and drop near-duplicates by cosine.
|
|
1073
|
+
// Fail-safe: if embeddings are unavailable, keep the string-deduped result.
|
|
1074
|
+
let semanticPatterns = dedupedPatterns;
|
|
1075
|
+
try {
|
|
1076
|
+
const { callOllamaEmbedBatch } = await import('../mcp/setup.js');
|
|
1077
|
+
const THRESHOLD = parseFloat(process.env.LEARN_DEDUP_THRESHOLD || '0.9');
|
|
1078
|
+
const candTexts = dedupedPatterns.map(p => p.rule);
|
|
1079
|
+
const embs = await callOllamaEmbedBatch([...candTexts, ...existingRules]);
|
|
1080
|
+
if (embs && embs.length === candTexts.length + existingRules.length) {
|
|
1081
|
+
const candEmbs = embs.slice(0, candTexts.length);
|
|
1082
|
+
const existEmbs = embs.slice(candTexts.length);
|
|
1083
|
+
const kept = [];
|
|
1084
|
+
const keptEmbs = [];
|
|
1085
|
+
for (let i = 0; i < dedupedPatterns.length; i++) {
|
|
1086
|
+
const e = candEmbs[i];
|
|
1087
|
+
const dup = existEmbs.some(xe => ClaudeCodeAdapter.cosine(e, xe) >= THRESHOLD)
|
|
1088
|
+
|| keptEmbs.some(ke => ClaudeCodeAdapter.cosine(e, ke) >= THRESHOLD);
|
|
1089
|
+
if (dup) {
|
|
1090
|
+
console.log(`[Self-Improve] Semantic dup dropped (>=${THRESHOLD}): "${dedupedPatterns[i].rule.substring(0, 80)}"`);
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
kept.push(dedupedPatterns[i]);
|
|
1094
|
+
keptEmbs.push(e);
|
|
1095
|
+
}
|
|
1096
|
+
semanticPatterns = kept;
|
|
1097
|
+
console.log(`[Self-Improve] Semantic dedup: ${dedupedPatterns.length} -> ${kept.length} (threshold ${THRESHOLD})`);
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
console.log(`[Self-Improve] Embeddings unavailable — falling back to string dedup`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
catch (e) {
|
|
1104
|
+
console.warn(`[Self-Improve] Semantic dedup error, using string dedup:`, e instanceof Error ? e.message : e);
|
|
1105
|
+
}
|
|
1106
|
+
if (semanticPatterns.length === 0) {
|
|
1107
|
+
console.log(`[Self-Improve] All new patterns are semantic duplicates of existing rules`);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1060
1110
|
// Cap at 3 new rules per run, and respect total max
|
|
1061
|
-
const toAdd =
|
|
1111
|
+
const toAdd = semanticPatterns.slice(0, Math.min(3, slotsAvailable));
|
|
1062
1112
|
// Append new rules at the END of the Learned Rules section
|
|
1063
1113
|
const rulesBlock = toAdd.map(p => `- ${p.rule} (confidence: ${p.confidence})`).join('\n');
|
|
1064
1114
|
if (existingContent.includes(SECTION_HEADER)) {
|
package/dist/mcp/setup.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare function callMem0Patterns(agentName: string, minCluster?: number)
|
|
|
20
20
|
confidence: number;
|
|
21
21
|
sources: string[];
|
|
22
22
|
}>>;
|
|
23
|
+
export declare function callOllamaEmbedBatch(texts: string[]): Promise<number[][] | null>;
|
|
23
24
|
/**
|
|
24
25
|
* Generate AI-powered reflection from task results using Claude CLI (-p mode).
|
|
25
26
|
* Spawns `claude -p <prompt>` locally — uses existing Claude subscription, no extra cost.
|
package/dist/mcp/setup.js
CHANGED
|
@@ -268,6 +268,61 @@ export async function callMem0Patterns(agentName, minCluster = 3) {
|
|
|
268
268
|
child.stdin?.end();
|
|
269
269
|
});
|
|
270
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Embed an array of texts via Ollama batch endpoint (/api/embed), using the same
|
|
273
|
+
* transport as Mem0: direct HTTP in local mode, SSH+curl for remote agents.
|
|
274
|
+
* Used by ACE-style semantic de-duplication of learned rules. Returns null on any
|
|
275
|
+
* failure so callers can fall back to string matching (no regression).
|
|
276
|
+
*/
|
|
277
|
+
const EMBED_MODEL = process.env.LEARN_EMBED_MODEL || 'mxbai-embed-large';
|
|
278
|
+
export async function callOllamaEmbedBatch(texts) {
|
|
279
|
+
if (texts.length === 0)
|
|
280
|
+
return [];
|
|
281
|
+
const body = JSON.stringify({ model: EMBED_MODEL, input: texts });
|
|
282
|
+
try {
|
|
283
|
+
if (MEM0_SSH_HOST && MEM0_SSH_HOST !== 'local') {
|
|
284
|
+
// Remote agent: SSH then curl Ollama on the Mem0 host (127.0.0.1:11434)
|
|
285
|
+
const { execFile } = await import('child_process');
|
|
286
|
+
const escaped = body.replace(/'/g, "'\\''");
|
|
287
|
+
const remoteCmd = `curl -s -m 60 -X POST http://127.0.0.1:11434/api/embed -H 'Content-Type: application/json' -d '${escaped}'`;
|
|
288
|
+
const args = ['-p', MEM0_SSH_PORT, '-o', 'StrictHostKeyChecking=accept-new'];
|
|
289
|
+
if (MEM0_SSH_KEY)
|
|
290
|
+
args.push('-i', MEM0_SSH_KEY);
|
|
291
|
+
args.push(MEM0_SSH_HOST, remoteCmd);
|
|
292
|
+
return await new Promise((resolve) => {
|
|
293
|
+
execFile('ssh', args, { timeout: 90000, maxBuffer: 32 * 1024 * 1024 }, (err, stdout) => {
|
|
294
|
+
if (err) {
|
|
295
|
+
console.warn(`[Embed] SSH failed: ${err.message}`);
|
|
296
|
+
resolve(null);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
resolve(JSON.parse(stdout.trim()).embeddings ?? null);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
resolve(null);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
// Local mode or HTTP base: direct call
|
|
309
|
+
const url = `${MEM0_ENV_VARS.MEM0_OLLAMA_URL}/api/embed`;
|
|
310
|
+
const res = await fetch(url, {
|
|
311
|
+
method: 'POST',
|
|
312
|
+
headers: { 'Content-Type': 'application/json' },
|
|
313
|
+
body,
|
|
314
|
+
signal: AbortSignal.timeout(60000),
|
|
315
|
+
});
|
|
316
|
+
if (!res.ok)
|
|
317
|
+
return null;
|
|
318
|
+
const data = await res.json();
|
|
319
|
+
return data.embeddings ?? null;
|
|
320
|
+
}
|
|
321
|
+
catch (e) {
|
|
322
|
+
console.warn(`[Embed] Failed:`, e instanceof Error ? e.message : e);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
271
326
|
/**
|
|
272
327
|
* Generate AI-powered reflection from task results using Claude CLI (-p mode).
|
|
273
328
|
* Spawns `claude -p <prompt>` locally — uses existing Claude subscription, no extra cost.
|
package/dist/utils/claude-cli.js
CHANGED
|
@@ -119,9 +119,13 @@ export function runClaude(options) {
|
|
|
119
119
|
if (options.disallowedTools?.length) {
|
|
120
120
|
args.push('--disallowedTools', options.disallowedTools.join(','));
|
|
121
121
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
// Always steer agents to answer the user in Vietnamese, regardless of the
|
|
123
|
+
// language of the task/skill/context. Merged with any caller system prompt.
|
|
124
|
+
const LANG_DIRECTIVE = 'QUAN TRỌNG — NGÔN NGỮ PHẢN HỒI: Luôn trả lời, báo cáo và giao tiếp với người dùng bằng TIẾNG VIỆT, kể cả khi task, skill, code, tài liệu hay context viết bằng tiếng Anh. Giữ nguyên thuật ngữ kỹ thuật, tên riêng, lệnh, code; phần diễn giải/kết luận phải bằng tiếng Việt.';
|
|
125
|
+
const mergedSystemPrompt = options.systemPrompt
|
|
126
|
+
? `${options.systemPrompt}\n\n${LANG_DIRECTIVE}`
|
|
127
|
+
: LANG_DIRECTIVE;
|
|
128
|
+
args.push('--append-system-prompt', mergedSystemPrompt);
|
|
125
129
|
if (options.maxTurns) {
|
|
126
130
|
args.push('--max-turns', String(options.maxTurns));
|
|
127
131
|
}
|