tuna-agent 0.1.54 → 0.1.56
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.
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { AgentAdapter, AgentType } from './types.js';
|
|
2
2
|
import type { AgentConfig, TaskAssignment, InputResponse } from '../types/index.js';
|
|
3
3
|
import type { AgentWebSocketClient } from '../daemon/ws-client.js';
|
|
4
|
+
export interface LearnedRule {
|
|
5
|
+
content: string;
|
|
6
|
+
confidence: number;
|
|
7
|
+
sourceCount: number;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
}
|
|
4
10
|
export interface AgentMetrics {
|
|
5
11
|
taskCount: number;
|
|
6
12
|
successCount: number;
|
|
@@ -25,6 +31,8 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
25
31
|
private taskCount;
|
|
26
32
|
private static readonly PATTERN_CHECK_INTERVAL;
|
|
27
33
|
private metricsMap;
|
|
34
|
+
private learnedRulesMap;
|
|
35
|
+
private agentFolderMap;
|
|
28
36
|
private currentAgentId;
|
|
29
37
|
/** Folder basename of current agent (e.g. "co-founder"). Used as Mem0 user_id. */
|
|
30
38
|
private currentAgentName;
|
|
@@ -33,6 +41,8 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
33
41
|
constructor(config: AgentConfig);
|
|
34
42
|
/** Get all per-agent metrics for heartbeat. */
|
|
35
43
|
getMetrics(): Record<string, unknown>;
|
|
44
|
+
/** Register an agent folder path for rules parsing (called from daemon). */
|
|
45
|
+
registerAgentFolder(agentId: string, folderPath: string): void;
|
|
36
46
|
checkHealth(): Promise<{
|
|
37
47
|
ok: boolean;
|
|
38
48
|
message: string;
|
|
@@ -58,6 +68,11 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
58
68
|
* Runs every N tasks to evolve the agent's permanent knowledge.
|
|
59
69
|
*/
|
|
60
70
|
runSelfImprovement(cwd: string): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Parse "## Learned Rules" section from CLAUDE.md and store in learnedRulesMap.
|
|
73
|
+
* Rule format: `- Rule text (confidence: 0.7)`
|
|
74
|
+
*/
|
|
75
|
+
private parseLearnedRules;
|
|
61
76
|
/** Track task completion metrics (public for daemon resume path). */
|
|
62
77
|
trackMetricsPublic(status: 'done' | 'failed', durationMs: number): void;
|
|
63
78
|
/** Track task completion metrics. */
|
|
@@ -16,6 +16,8 @@ export class ClaudeCodeAdapter {
|
|
|
16
16
|
static PATTERN_CHECK_INTERVAL = 1; // Check patterns every N tasks
|
|
17
17
|
// Per-agent state (one daemon handles multiple agents)
|
|
18
18
|
metricsMap = new Map();
|
|
19
|
+
learnedRulesMap = new Map();
|
|
20
|
+
agentFolderMap = new Map(); // agentId -> folder path
|
|
19
21
|
currentAgentId = '';
|
|
20
22
|
/** Folder basename of current agent (e.g. "co-founder"). Used as Mem0 user_id. */
|
|
21
23
|
currentAgentName = '';
|
|
@@ -47,12 +49,28 @@ export class ClaudeCodeAdapter {
|
|
|
47
49
|
}
|
|
48
50
|
/** Get all per-agent metrics for heartbeat. */
|
|
49
51
|
getMetrics() {
|
|
52
|
+
// Re-parse rules from CLAUDE.md for all known agents
|
|
53
|
+
this.agentFolderMap.forEach((folder, agentId) => {
|
|
54
|
+
const savedId = this.currentAgentId;
|
|
55
|
+
this.currentAgentId = agentId;
|
|
56
|
+
this.parseLearnedRules(path.join(folder, 'CLAUDE.md'));
|
|
57
|
+
this.currentAgentId = savedId;
|
|
58
|
+
});
|
|
50
59
|
const agentMetricsMap = {};
|
|
51
60
|
this.metricsMap.forEach((m, agentId) => {
|
|
52
|
-
|
|
61
|
+
const entry = { ...m };
|
|
62
|
+
const rules = this.learnedRulesMap.get(agentId);
|
|
63
|
+
if (rules && rules.length > 0) {
|
|
64
|
+
entry.learnedRules = rules;
|
|
65
|
+
}
|
|
66
|
+
agentMetricsMap[agentId] = entry;
|
|
53
67
|
});
|
|
54
68
|
return { agentMetricsMap };
|
|
55
69
|
}
|
|
70
|
+
/** Register an agent folder path for rules parsing (called from daemon). */
|
|
71
|
+
registerAgentFolder(agentId, folderPath) {
|
|
72
|
+
this.agentFolderMap.set(agentId, folderPath);
|
|
73
|
+
}
|
|
56
74
|
async checkHealth() {
|
|
57
75
|
try {
|
|
58
76
|
execSync('which claude', { stdio: 'ignore' });
|
|
@@ -91,6 +109,9 @@ export class ClaudeCodeAdapter {
|
|
|
91
109
|
this.currentAgentId = task.agentId || '';
|
|
92
110
|
const defaultWorkspaceEarly = path.join(os.homedir(), 'tuna-workspace');
|
|
93
111
|
this.currentAgentName = path.basename(task.repoPath || defaultWorkspaceEarly);
|
|
112
|
+
// Track agent folder for rules parsing in heartbeat
|
|
113
|
+
const cwd = task.repoPath || defaultWorkspaceEarly;
|
|
114
|
+
this.agentFolderMap.set(this.currentAgentId, cwd);
|
|
94
115
|
if (task.mode !== 'tuna') {
|
|
95
116
|
console.log(`[ClaudeCode] Agent Team mode — direct chat with Claude CLI`);
|
|
96
117
|
ws.sendProgress(task.id, 'executing', { startedAt: new Date().toISOString() });
|
|
@@ -848,11 +869,71 @@ export class ClaudeCodeAdapter {
|
|
|
848
869
|
this.metrics.latestLearnedRule = newPatterns[newPatterns.length - 1].rule.substring(0, 500);
|
|
849
870
|
console.log(`[Self-Improve] Added ${newPatterns.length} new rules to CLAUDE.md:`);
|
|
850
871
|
newPatterns.forEach(p => console.log(`[Self-Improve] - ${p.rule}`));
|
|
872
|
+
// Sync learned rules to heartbeat metrics
|
|
873
|
+
this.parseLearnedRules(claudeMdPath);
|
|
851
874
|
}
|
|
852
875
|
catch (err) {
|
|
853
876
|
console.warn(`[Self-Improve] Failed:`, err instanceof Error ? err.message : err);
|
|
854
877
|
}
|
|
855
878
|
}
|
|
879
|
+
/**
|
|
880
|
+
* Parse "## Learned Rules" section from CLAUDE.md and store in learnedRulesMap.
|
|
881
|
+
* Rule format: `- Rule text (confidence: 0.7)`
|
|
882
|
+
*/
|
|
883
|
+
parseLearnedRules(claudeMdPath) {
|
|
884
|
+
try {
|
|
885
|
+
if (!fs.existsSync(claudeMdPath))
|
|
886
|
+
return;
|
|
887
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
888
|
+
const SECTION_HEADER = '## Learned Rules';
|
|
889
|
+
const sectionIdx = content.indexOf(SECTION_HEADER);
|
|
890
|
+
if (sectionIdx === -1)
|
|
891
|
+
return;
|
|
892
|
+
const sectionContent = content.substring(sectionIdx + SECTION_HEADER.length);
|
|
893
|
+
// End at next ## heading or end of file
|
|
894
|
+
const nextSection = sectionContent.indexOf('\n## ');
|
|
895
|
+
const rulesText = nextSection !== -1 ? sectionContent.substring(0, nextSection) : sectionContent;
|
|
896
|
+
const rules = [];
|
|
897
|
+
const ruleRegex = /^- (.+?)(?:\s*\(confidence:\s*([\d.]+)\))?$/;
|
|
898
|
+
// First pass: collect raw confidence values
|
|
899
|
+
const rawEntries = [];
|
|
900
|
+
for (const line of rulesText.split('\n')) {
|
|
901
|
+
const trimmed = line.trim();
|
|
902
|
+
if (!trimmed || !trimmed.startsWith('-'))
|
|
903
|
+
continue;
|
|
904
|
+
const match = ruleRegex.exec(trimmed);
|
|
905
|
+
if (match) {
|
|
906
|
+
rawEntries.push({
|
|
907
|
+
content: match[1].trim(),
|
|
908
|
+
rawConf: match[2] ? parseFloat(match[2]) : 0.3,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
// Normalize confidence: if any value > 1, treat as source_count and scale to 0-1
|
|
913
|
+
const maxConf = Math.max(...rawEntries.map(e => e.rawConf), 1);
|
|
914
|
+
const needsNormalization = maxConf > 1;
|
|
915
|
+
for (const entry of rawEntries) {
|
|
916
|
+
const sourceCount = needsNormalization ? Math.round(entry.rawConf) : 1;
|
|
917
|
+
const confidence = needsNormalization
|
|
918
|
+
? Math.min(Math.max(entry.rawConf / maxConf, 0.1), 1.0)
|
|
919
|
+
: entry.rawConf;
|
|
920
|
+
rules.push({
|
|
921
|
+
content: entry.content,
|
|
922
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
923
|
+
sourceCount,
|
|
924
|
+
createdAt: new Date().toISOString(),
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
if (rules.length > 0) {
|
|
928
|
+
this.learnedRulesMap.set(this.currentAgentId, rules);
|
|
929
|
+
this.metrics.rulesCount = rules.length;
|
|
930
|
+
console.log(`[Self-Improve] Parsed ${rules.length} rules from CLAUDE.md for heartbeat sync`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
catch (err) {
|
|
934
|
+
console.warn(`[Self-Improve] Failed to parse learned rules:`, err instanceof Error ? err.message : err);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
856
937
|
/** Track task completion metrics (public for daemon resume path). */
|
|
857
938
|
trackMetricsPublic(status, durationMs) {
|
|
858
939
|
this.trackMetrics(status, durationMs);
|
package/dist/daemon/index.js
CHANGED
|
@@ -264,6 +264,10 @@ export async function startDaemon(config) {
|
|
|
264
264
|
? path.join(os.homedir(), rawFolder.slice(1))
|
|
265
265
|
: rawFolder;
|
|
266
266
|
console.log(`[Daemon] Rescan skills for agent ${agentId}, folder: ${folder || '(none)'}`);
|
|
267
|
+
// Register folder for rules parsing in heartbeat
|
|
268
|
+
if (folder && agentId && adapter.type === 'claude-code') {
|
|
269
|
+
adapter.registerAgentFolder(agentId, folder);
|
|
270
|
+
}
|
|
267
271
|
const folders = folder ? [folder] : [];
|
|
268
272
|
const skills = scanSkills(wsPath, folders);
|
|
269
273
|
ws.send({ action: 'agent_skills_scanned', agent_id: agentId, skills });
|
|
@@ -297,6 +301,10 @@ export async function startDaemon(config) {
|
|
|
297
301
|
else {
|
|
298
302
|
console.log(`[Daemon] .claude/CLAUDE.md already exists, skipping`);
|
|
299
303
|
}
|
|
304
|
+
// Register folder for rules parsing in heartbeat
|
|
305
|
+
if (adapter.type === 'claude-code') {
|
|
306
|
+
adapter.registerAgentFolder(agentId, folderPath);
|
|
307
|
+
}
|
|
300
308
|
ws.send({ action: 'agent_folder_created', agent_id: agentId, success: true });
|
|
301
309
|
console.log(`[Daemon] Agent folder created: ${folderPath}`);
|
|
302
310
|
}
|