tuna-agent 0.1.59 → 0.1.60
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.
|
@@ -69,6 +69,10 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
69
69
|
* Self-Improvement: detect patterns from Mem0 and update CLAUDE.md.
|
|
70
70
|
* Runs every N tasks to evolve the agent's permanent knowledge.
|
|
71
71
|
*/
|
|
72
|
+
/** Extract key phrases (3+ word sequences) from a rule for similarity matching. */
|
|
73
|
+
private static extractKeyPhrases;
|
|
74
|
+
/** Check if two rules are semantically similar (>40% key phrase overlap). */
|
|
75
|
+
private static isSimilarRule;
|
|
72
76
|
runSelfImprovement(cwd: string): Promise<void>;
|
|
73
77
|
/**
|
|
74
78
|
* Parse "## Learned Rules" section from CLAUDE.md and store in learnedRulesMap.
|
|
@@ -846,6 +846,24 @@ export class ClaudeCodeAdapter {
|
|
|
846
846
|
* Self-Improvement: detect patterns from Mem0 and update CLAUDE.md.
|
|
847
847
|
* Runs every N tasks to evolve the agent's permanent knowledge.
|
|
848
848
|
*/
|
|
849
|
+
/** Extract key phrases (3+ word sequences) from a rule for similarity matching. */
|
|
850
|
+
static extractKeyPhrases(text) {
|
|
851
|
+
const words = text.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).filter(w => w.length > 2);
|
|
852
|
+
const phrases = [];
|
|
853
|
+
for (let i = 0; i <= words.length - 3; i++) {
|
|
854
|
+
phrases.push(words.slice(i, i + 3).join(' '));
|
|
855
|
+
}
|
|
856
|
+
return phrases;
|
|
857
|
+
}
|
|
858
|
+
/** Check if two rules are semantically similar (>40% key phrase overlap). */
|
|
859
|
+
static isSimilarRule(ruleA, ruleB) {
|
|
860
|
+
const phrasesA = ClaudeCodeAdapter.extractKeyPhrases(ruleA);
|
|
861
|
+
const phrasesB = new Set(ClaudeCodeAdapter.extractKeyPhrases(ruleB));
|
|
862
|
+
if (phrasesA.length === 0)
|
|
863
|
+
return false;
|
|
864
|
+
const overlap = phrasesA.filter(p => phrasesB.has(p)).length;
|
|
865
|
+
return overlap / phrasesA.length > 0.4;
|
|
866
|
+
}
|
|
849
867
|
async runSelfImprovement(cwd) {
|
|
850
868
|
if (!process.env.MEM0_SSH_HOST)
|
|
851
869
|
return;
|
|
@@ -860,41 +878,76 @@ export class ClaudeCodeAdapter {
|
|
|
860
878
|
console.log(`[Self-Improve] No patterns detected yet`);
|
|
861
879
|
return;
|
|
862
880
|
}
|
|
863
|
-
// Read current CLAUDE.md
|
|
881
|
+
// Read current CLAUDE.md and extract existing rules
|
|
864
882
|
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
865
883
|
let existingContent = '';
|
|
866
884
|
if (fs.existsSync(claudeMdPath)) {
|
|
867
885
|
existingContent = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
868
886
|
}
|
|
869
|
-
//
|
|
887
|
+
// Parse existing rules from "## Learned Rules" section
|
|
888
|
+
const SECTION_HEADER = '## Learned Rules';
|
|
889
|
+
const existingRules = [];
|
|
890
|
+
const sectionIdx = existingContent.indexOf(SECTION_HEADER);
|
|
891
|
+
if (sectionIdx !== -1) {
|
|
892
|
+
const sectionContent = existingContent.substring(sectionIdx + SECTION_HEADER.length);
|
|
893
|
+
const nextSection = sectionContent.indexOf('\n## ');
|
|
894
|
+
const rulesText = nextSection !== -1 ? sectionContent.substring(0, nextSection) : sectionContent;
|
|
895
|
+
for (const line of rulesText.split('\n')) {
|
|
896
|
+
const trimmed = line.trim();
|
|
897
|
+
if (trimmed.startsWith('- ')) {
|
|
898
|
+
// Strip confidence suffix
|
|
899
|
+
const ruleText = trimmed.replace(/\s*\(confidence:\s*[\d.]+\)\s*$/, '').substring(2);
|
|
900
|
+
if (ruleText)
|
|
901
|
+
existingRules.push(ruleText);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
// Filter: skip patterns similar to ANY existing rule
|
|
870
906
|
const newPatterns = patterns.filter(p => {
|
|
871
|
-
|
|
872
|
-
const ruleNormalized = p.rule.toLowerCase().replace(/[^a-z0-9\s]/g, '');
|
|
873
|
-
return !existingContent.toLowerCase().includes(ruleNormalized.substring(0, 50));
|
|
907
|
+
return !existingRules.some(existing => ClaudeCodeAdapter.isSimilarRule(p.rule, existing));
|
|
874
908
|
});
|
|
875
|
-
|
|
876
|
-
|
|
909
|
+
// Also dedup among new patterns themselves
|
|
910
|
+
const dedupedPatterns = [];
|
|
911
|
+
for (const p of newPatterns) {
|
|
912
|
+
const isDup = dedupedPatterns.some(existing => ClaudeCodeAdapter.isSimilarRule(p.rule, existing.rule));
|
|
913
|
+
if (!isDup)
|
|
914
|
+
dedupedPatterns.push(p);
|
|
915
|
+
}
|
|
916
|
+
if (dedupedPatterns.length === 0) {
|
|
917
|
+
console.log(`[Self-Improve] ${patterns.length} patterns found but all similar to existing rules`);
|
|
877
918
|
return;
|
|
878
919
|
}
|
|
879
|
-
//
|
|
880
|
-
const
|
|
881
|
-
|
|
920
|
+
// Cap at 3 new rules per run to avoid bloating
|
|
921
|
+
const toAdd = dedupedPatterns.slice(0, 3);
|
|
922
|
+
// Append new rules at the END of the Learned Rules section
|
|
923
|
+
const rulesBlock = toAdd.map(p => `- ${p.rule} (confidence: ${p.confidence})`).join('\n');
|
|
882
924
|
if (existingContent.includes(SECTION_HEADER)) {
|
|
883
|
-
//
|
|
884
|
-
const
|
|
885
|
-
|
|
925
|
+
// Find end of Learned Rules section (next ## or end of file)
|
|
926
|
+
const headerIdx = existingContent.indexOf(SECTION_HEADER);
|
|
927
|
+
const afterHeader = existingContent.substring(headerIdx + SECTION_HEADER.length);
|
|
928
|
+
const nextSectionIdx = afterHeader.indexOf('\n## ');
|
|
929
|
+
if (nextSectionIdx !== -1) {
|
|
930
|
+
// Insert before next section
|
|
931
|
+
const insertPos = headerIdx + SECTION_HEADER.length + nextSectionIdx;
|
|
932
|
+
const updatedContent = existingContent.substring(0, insertPos) + '\n' + rulesBlock + existingContent.substring(insertPos);
|
|
933
|
+
fs.writeFileSync(claudeMdPath, updatedContent);
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
// Append at end of file
|
|
937
|
+
const ending = existingContent.endsWith('\n') ? '' : '\n';
|
|
938
|
+
fs.writeFileSync(claudeMdPath, existingContent + ending + rulesBlock + '\n');
|
|
939
|
+
}
|
|
886
940
|
}
|
|
887
941
|
else {
|
|
888
|
-
// Add new section at the end
|
|
889
942
|
const separator = existingContent.endsWith('\n') ? '\n' : '\n\n';
|
|
890
943
|
fs.writeFileSync(claudeMdPath, existingContent + separator + `${SECTION_HEADER}\n${rulesBlock}\n`);
|
|
891
944
|
}
|
|
892
|
-
this.metrics.patternsLearnedCount +=
|
|
893
|
-
this.metrics.rulesCount +=
|
|
945
|
+
this.metrics.patternsLearnedCount += toAdd.length;
|
|
946
|
+
this.metrics.rulesCount += toAdd.length;
|
|
894
947
|
this.metrics.lastPatternAt = new Date().toISOString();
|
|
895
|
-
this.metrics.latestLearnedRule =
|
|
896
|
-
console.log(`[Self-Improve] Added ${
|
|
897
|
-
|
|
948
|
+
this.metrics.latestLearnedRule = toAdd[toAdd.length - 1].rule.substring(0, 500);
|
|
949
|
+
console.log(`[Self-Improve] Added ${toAdd.length} new rules to CLAUDE.md:`);
|
|
950
|
+
toAdd.forEach(p => console.log(`[Self-Improve] - ${p.rule}`));
|
|
898
951
|
// Sync learned rules to heartbeat metrics
|
|
899
952
|
this.parseLearnedRules(claudeMdPath);
|
|
900
953
|
}
|