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 = dedupedPatterns.slice(0, Math.min(3, slotsAvailable));
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)) {
@@ -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.
@@ -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
- if (options.systemPrompt) {
123
- args.push('--append-system-prompt', options.systemPrompt);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.160",
3
+ "version": "0.1.162",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"