tuna-agent 0.1.181 → 0.1.182
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.
|
@@ -57,7 +57,7 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
57
57
|
* Run post-task self-reflection via Mem0.
|
|
58
58
|
* Agent reviews what it did and stores lessons learned.
|
|
59
59
|
*/
|
|
60
|
-
runReflection(task: TaskAssignment, resultSummary: string, status: 'done' | 'failed', cwd: string): Promise<void>;
|
|
60
|
+
runReflection(task: TaskAssignment, resultSummary: string, status: 'done' | 'failed', cwd: string, conversation?: string[]): Promise<void>;
|
|
61
61
|
/**
|
|
62
62
|
* Store user rating feedback into Mem0.
|
|
63
63
|
*/
|
|
@@ -181,6 +181,7 @@ export class ClaudeCodeAdapter {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
let lastTaskOutput = ''; // Track last output for reflection
|
|
184
|
+
const convo = []; // User/Agent transcript → conversation memory (Mem0)
|
|
184
185
|
try {
|
|
185
186
|
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
186
187
|
let streamMsgId = `team-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -204,6 +205,7 @@ export class ClaudeCodeAdapter {
|
|
|
204
205
|
return false;
|
|
205
206
|
};
|
|
206
207
|
console.log(`[ClaudeCode] Agent Team round ${round + 1}: ${userMessage.substring(0, 80)}${currentInputFiles?.length ? ` (+${currentInputFiles.length} images)` : ''}`);
|
|
208
|
+
convo.push(`User: ${userMessage}`);
|
|
207
209
|
const defaultWorkspace = path.join(os.homedir(), 'tuna-workspace');
|
|
208
210
|
const cwd = task.repoPath || defaultWorkspace;
|
|
209
211
|
if (!fs.existsSync(cwd))
|
|
@@ -346,6 +348,8 @@ export class ClaudeCodeAdapter {
|
|
|
346
348
|
}
|
|
347
349
|
// Track last output for reflection (fallback to result.result if no streaming text)
|
|
348
350
|
lastTaskOutput = turnAccumulatedText.trim() || result.result;
|
|
351
|
+
if (lastTaskOutput)
|
|
352
|
+
convo.push(`Agent: ${lastTaskOutput}`);
|
|
349
353
|
if (lastTaskOutput) {
|
|
350
354
|
console.log(`[Reflection] Captured ${lastTaskOutput.length} chars of task output for reflection`);
|
|
351
355
|
}
|
|
@@ -415,7 +419,7 @@ export class ClaudeCodeAdapter {
|
|
|
415
419
|
});
|
|
416
420
|
this.trackMetrics('done', totalDurationMs, localAgentId);
|
|
417
421
|
const timeoutOutput = lastTaskOutput || 'Task completed (no follow-up)';
|
|
418
|
-
this.runReflection(task, timeoutOutput, 'done', task.repoPath)
|
|
422
|
+
this.runReflection(task, timeoutOutput, 'done', task.repoPath, convo)
|
|
419
423
|
.then(() => this.runSelfImprovement(task.repoPath, localAgentId))
|
|
420
424
|
.catch(() => { });
|
|
421
425
|
return;
|
|
@@ -448,7 +452,7 @@ export class ClaudeCodeAdapter {
|
|
|
448
452
|
this.trackMetrics('done', totalDurationMs, localAgentId);
|
|
449
453
|
console.log(`[ClaudeCode] Agent Team task ${task.id} completed (${(totalDurationMs / 1000).toFixed(1)}s)`);
|
|
450
454
|
// Post-task reflection with actual output (non-blocking)
|
|
451
|
-
this.runReflection(task, lastTaskOutput || 'Task completed without text output', 'done', task.repoPath)
|
|
455
|
+
this.runReflection(task, lastTaskOutput || 'Task completed without text output', 'done', task.repoPath, convo)
|
|
452
456
|
.then(() => this.runSelfImprovement(task.repoPath, localAgentId))
|
|
453
457
|
.catch(() => { });
|
|
454
458
|
}
|
|
@@ -810,7 +814,7 @@ export class ClaudeCodeAdapter {
|
|
|
810
814
|
* Run post-task self-reflection via Mem0.
|
|
811
815
|
* Agent reviews what it did and stores lessons learned.
|
|
812
816
|
*/
|
|
813
|
-
async runReflection(task, resultSummary, status, cwd) {
|
|
817
|
+
async runReflection(task, resultSummary, status, cwd, conversation) {
|
|
814
818
|
if (task.enableReflection === false)
|
|
815
819
|
return;
|
|
816
820
|
if (!process.env.MEM0_SSH_HOST)
|
|
@@ -820,6 +824,16 @@ export class ClaudeCodeAdapter {
|
|
|
820
824
|
return;
|
|
821
825
|
const agentName = path.basename(cwd);
|
|
822
826
|
const agentId = task.agentId || '';
|
|
827
|
+
// Conversation memory: distill what was DISCUSSED (topics, decisions, user
|
|
828
|
+
// preferences) into Mem0 — independent of the lesson reflection below, so the
|
|
829
|
+
// agent remembers past conversations without manually writing .md notes.
|
|
830
|
+
if (conversation && conversation.length >= 2) {
|
|
831
|
+
import('../mcp/setup.js')
|
|
832
|
+
.then(({ storeConversationFacts }) => storeConversationFacts(conversation, agentName))
|
|
833
|
+
.then((n) => { if (n)
|
|
834
|
+
this.getMetricsForAgent(agentId).memoryCount += n; })
|
|
835
|
+
.catch(() => { });
|
|
836
|
+
}
|
|
823
837
|
// Record a quality score for this task — the signal the self-improvement loop
|
|
824
838
|
// optimizes against (closes the loop: score in -> trend gates rule changes out).
|
|
825
839
|
const runScore = ClaudeCodeAdapter.deriveScore(status, resultSummary);
|
package/dist/daemon/index.js
CHANGED
|
@@ -745,6 +745,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
745
745
|
taskAbortControllers.set(taskId, abort);
|
|
746
746
|
let pmSessionId = pmState.pmSessionId;
|
|
747
747
|
const MAX_RESUMED_ROUNDS = 50;
|
|
748
|
+
const convo = []; // User/Agent transcript → conversation memory (Mem0)
|
|
748
749
|
try {
|
|
749
750
|
let userMessage = firstMessage;
|
|
750
751
|
let currentAttachments = firstAttachments;
|
|
@@ -761,6 +762,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
761
762
|
}
|
|
762
763
|
}
|
|
763
764
|
console.log(`[Daemon] Resumed PM chat round ${round + 1}: ${userMessage.substring(0, 80)}${inputFiles?.length ? ` (+${inputFiles.length} images)` : ''}`);
|
|
765
|
+
convo.push(`User: ${userMessage}`);
|
|
764
766
|
const streamMsgId = `pm-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
765
767
|
let firstChunkIso = '';
|
|
766
768
|
const chatResult = await chatWithPM(pmSessionId, pmState.repoPath, userMessage, abort.signal, (chunk) => {
|
|
@@ -774,6 +776,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
774
776
|
// PM produced a plan → execute it using shared helper
|
|
775
777
|
if (chatResult.plan && chatResult.plan.subtasks.length > 0) {
|
|
776
778
|
const plan = chatResult.plan;
|
|
779
|
+
convo.push(`Agent: (bắt tay thực hiện) ${plan.summary}`);
|
|
777
780
|
// (No "Got it! <summary>" — plan_ready already conveys the summary.)
|
|
778
781
|
wsClient.sendPlanReady(taskId, {
|
|
779
782
|
summary: plan.summary,
|
|
@@ -808,6 +811,8 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
808
811
|
return;
|
|
809
812
|
}
|
|
810
813
|
// Send final PM message
|
|
814
|
+
if (chatResult.response?.trim())
|
|
815
|
+
convo.push(`Agent: ${chatResult.response}`);
|
|
811
816
|
wsClient.sendPMMessage(taskId, { sender: 'pm', content: chatResult.response, startedAt: firstChunkIso || undefined });
|
|
812
817
|
if (round === MAX_RESUMED_ROUNDS - 1)
|
|
813
818
|
break;
|
|
@@ -831,6 +836,12 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
831
836
|
}
|
|
832
837
|
}
|
|
833
838
|
finally {
|
|
839
|
+
// Conversation memory: remember what was discussed across sessions (non-blocking).
|
|
840
|
+
if (convo.length >= 2) {
|
|
841
|
+
import('../mcp/setup.js')
|
|
842
|
+
.then(({ storeConversationFacts }) => storeConversationFacts(convo, path.basename(pmState.repoPath)))
|
|
843
|
+
.catch(() => { });
|
|
844
|
+
}
|
|
834
845
|
activeTasks--;
|
|
835
846
|
taskAbortControllers.delete(taskId);
|
|
836
847
|
resolvers.delete(taskId);
|
|
@@ -850,6 +861,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
850
861
|
taskAbortControllers.set(taskId, abort);
|
|
851
862
|
let sessionId = savedState.agentTeamSessionId;
|
|
852
863
|
let totalDurationMs = 0;
|
|
864
|
+
const convo = []; // User/Agent transcript → conversation memory (Mem0)
|
|
853
865
|
try {
|
|
854
866
|
let userMessage = firstMessage;
|
|
855
867
|
let currentInputFiles;
|
|
@@ -873,6 +885,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
873
885
|
let turnAccumulatedText = '';
|
|
874
886
|
let messageCount = 0;
|
|
875
887
|
console.log(`[Daemon] Resumed agent_team round ${round + 1}: ${userMessage.substring(0, 80)}${currentInputFiles?.length ? ` (+${currentInputFiles.length} images)` : ''}`);
|
|
888
|
+
convo.push(`User: ${userMessage}`);
|
|
876
889
|
let result;
|
|
877
890
|
try {
|
|
878
891
|
result = await runClaude({
|
|
@@ -999,6 +1012,8 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
999
1012
|
});
|
|
1000
1013
|
}
|
|
1001
1014
|
lastResumeOutput = turnAccumulatedText.trim() || resultText || lastResumeOutput;
|
|
1015
|
+
if (turnAccumulatedText.trim() || resultText)
|
|
1016
|
+
convo.push(`Agent: ${turnAccumulatedText.trim() || resultText}`);
|
|
1002
1017
|
if (result.isError) {
|
|
1003
1018
|
wsClient.sendTaskFailed(taskId, result.result);
|
|
1004
1019
|
return;
|
|
@@ -1076,6 +1091,12 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
1076
1091
|
}
|
|
1077
1092
|
}
|
|
1078
1093
|
finally {
|
|
1094
|
+
// Conversation memory: remember what was discussed across sessions (non-blocking).
|
|
1095
|
+
if (convo.length >= 2) {
|
|
1096
|
+
import('../mcp/setup.js')
|
|
1097
|
+
.then(({ storeConversationFacts }) => storeConversationFacts(convo, path.basename(savedState.repoPath)))
|
|
1098
|
+
.catch(() => { });
|
|
1099
|
+
}
|
|
1079
1100
|
activeTasks--;
|
|
1080
1101
|
taskAbortControllers.delete(taskId);
|
|
1081
1102
|
resolvers.delete(taskId);
|
package/dist/mcp/setup.d.ts
CHANGED
|
@@ -21,12 +21,14 @@ export declare function callMem0Patterns(agentName: string, minCluster?: number)
|
|
|
21
21
|
sources: string[];
|
|
22
22
|
}>>;
|
|
23
23
|
export declare function callOllamaEmbedBatch(texts: string[]): Promise<number[][] | null>;
|
|
24
|
+
export declare function callMem0Reflect(taskDesc: string, resultSummary: string, status: 'done' | 'failed'): Promise<string>;
|
|
24
25
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* Distill a finished conversation into durable facts (topics discussed, decisions,
|
|
27
|
+
* user preferences/interests) and store them in Mem0 — so the agent "remembers"
|
|
28
|
+
* past conversations in future sessions WITHOUT relying on it manually writing
|
|
29
|
+
* notes to a .md file. Returns the number of facts stored.
|
|
28
30
|
*/
|
|
29
|
-
export declare function
|
|
31
|
+
export declare function storeConversationFacts(transcript: string[], agentName: string): Promise<number>;
|
|
30
32
|
/**
|
|
31
33
|
* Generate MCP server config file for Claude Code.
|
|
32
34
|
* This file is auto-detected by runClaude and passed via --mcp-config.
|
package/dist/mcp/setup.js
CHANGED
|
@@ -328,8 +328,9 @@ export async function callOllamaEmbedBatch(texts) {
|
|
|
328
328
|
* Spawns `claude -p <prompt>` locally — uses existing Claude subscription, no extra cost.
|
|
329
329
|
* Returns a concise lesson learned, or empty string on failure.
|
|
330
330
|
*/
|
|
331
|
-
|
|
332
|
-
|
|
331
|
+
/** Run a one-shot `claude -p` text prompt (used for reflection/distillation). */
|
|
332
|
+
async function claudeTextOnce(prompt, tag, timeoutMs = 60000) {
|
|
333
|
+
const { execSync, spawn } = await import('child_process');
|
|
333
334
|
// Resolve claude binary path (same search logic as claude-cli.ts)
|
|
334
335
|
let claudeBin = 'claude';
|
|
335
336
|
try {
|
|
@@ -348,25 +349,7 @@ export async function callMem0Reflect(taskDesc, resultSummary, status) {
|
|
|
348
349
|
}).trim();
|
|
349
350
|
}
|
|
350
351
|
catch { /* fall back to 'claude' */ }
|
|
351
|
-
const prompt = [
|
|
352
|
-
'You are an AI agent reflecting on a completed task.',
|
|
353
|
-
'Extract 1-2 concise, actionable lessons learned that would help with similar future tasks.',
|
|
354
|
-
'',
|
|
355
|
-
`Task: ${taskDesc.substring(0, 300)}`,
|
|
356
|
-
`Status: ${status}`,
|
|
357
|
-
`Result summary: ${resultSummary.substring(0, 600)}`,
|
|
358
|
-
'',
|
|
359
|
-
'Rules:',
|
|
360
|
-
'- ALWAYS extract at least 1 lesson — there is always something to note',
|
|
361
|
-
'- Be specific: tool names, patterns, pitfalls, or key findings worth remembering',
|
|
362
|
-
'- If task failed: focus on what went wrong and how to avoid it next time',
|
|
363
|
-
'- If task succeeded: note the approach or insight that was most useful',
|
|
364
|
-
'- Keep each lesson to 1 sentence, no bullet points, no preamble',
|
|
365
|
-
'',
|
|
366
|
-
'Lessons learned:',
|
|
367
|
-
].join('\n');
|
|
368
352
|
// Use spawn with stdin='ignore' — execFile keeps stdin pipe open which causes claude to hang/SIGTERM
|
|
369
|
-
const { spawn } = await import('child_process');
|
|
370
353
|
return new Promise((resolve) => {
|
|
371
354
|
const child = spawn(claudeBin, ['-p', prompt, '--output-format', 'text'], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
372
355
|
let stdout = '';
|
|
@@ -375,31 +358,99 @@ export async function callMem0Reflect(taskDesc, resultSummary, status) {
|
|
|
375
358
|
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
376
359
|
const timer = setTimeout(() => {
|
|
377
360
|
child.kill();
|
|
378
|
-
console.warn(
|
|
361
|
+
console.warn(`[${tag}] Claude CLI timed out after ${timeoutMs / 1000}s`);
|
|
379
362
|
resolve('');
|
|
380
|
-
},
|
|
363
|
+
}, timeoutMs);
|
|
381
364
|
child.on('close', (code) => {
|
|
382
365
|
clearTimeout(timer);
|
|
383
366
|
if (code !== 0) {
|
|
384
|
-
console.warn(`[
|
|
385
|
-
resolve('');
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
const reflection = stdout.trim();
|
|
389
|
-
if (!reflection) {
|
|
367
|
+
console.warn(`[${tag}] Claude CLI exited ${code}: ${stderr.substring(0, 200)}`);
|
|
390
368
|
resolve('');
|
|
391
369
|
return;
|
|
392
370
|
}
|
|
393
|
-
|
|
394
|
-
resolve(reflection);
|
|
371
|
+
resolve(stdout.trim());
|
|
395
372
|
});
|
|
396
373
|
child.on('error', (err) => {
|
|
397
374
|
clearTimeout(timer);
|
|
398
|
-
console.warn(`[
|
|
375
|
+
console.warn(`[${tag}] Spawn error: ${err.message}`);
|
|
399
376
|
resolve('');
|
|
400
377
|
});
|
|
401
378
|
});
|
|
402
379
|
}
|
|
380
|
+
export async function callMem0Reflect(taskDesc, resultSummary, status) {
|
|
381
|
+
const prompt = [
|
|
382
|
+
'You are an AI agent reflecting on a completed task.',
|
|
383
|
+
'Extract 1-2 concise, actionable lessons learned that would help with similar future tasks.',
|
|
384
|
+
'',
|
|
385
|
+
`Task: ${taskDesc.substring(0, 300)}`,
|
|
386
|
+
`Status: ${status}`,
|
|
387
|
+
`Result summary: ${resultSummary.substring(0, 600)}`,
|
|
388
|
+
'',
|
|
389
|
+
'Rules:',
|
|
390
|
+
'- ALWAYS extract at least 1 lesson — there is always something to note',
|
|
391
|
+
'- Be specific: tool names, patterns, pitfalls, or key findings worth remembering',
|
|
392
|
+
'- If task failed: focus on what went wrong and how to avoid it next time',
|
|
393
|
+
'- If task succeeded: note the approach or insight that was most useful',
|
|
394
|
+
'- Keep each lesson to 1 sentence, no bullet points, no preamble',
|
|
395
|
+
'',
|
|
396
|
+
'Lessons learned:',
|
|
397
|
+
].join('\n');
|
|
398
|
+
const reflection = await claudeTextOnce(prompt, 'Mem0 Reflect');
|
|
399
|
+
if (reflection)
|
|
400
|
+
console.log(`[Mem0 Reflect] Generated: "${reflection.substring(0, 100)}..."`);
|
|
401
|
+
return reflection;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Distill a finished conversation into durable facts (topics discussed, decisions,
|
|
405
|
+
* user preferences/interests) and store them in Mem0 — so the agent "remembers"
|
|
406
|
+
* past conversations in future sessions WITHOUT relying on it manually writing
|
|
407
|
+
* notes to a .md file. Returns the number of facts stored.
|
|
408
|
+
*/
|
|
409
|
+
export async function storeConversationFacts(transcript, agentName) {
|
|
410
|
+
if (!process.env.MEM0_SSH_HOST)
|
|
411
|
+
return 0;
|
|
412
|
+
if (!transcript || transcript.length < 2)
|
|
413
|
+
return 0; // need at least one exchange
|
|
414
|
+
// Cap each message and the total so the distill prompt stays small.
|
|
415
|
+
const capped = transcript.map((m) => m.length > 400 ? m.slice(0, 400) + '…' : m);
|
|
416
|
+
let convoText = capped.join('\n');
|
|
417
|
+
if (convoText.length > 6000)
|
|
418
|
+
convoText = convoText.slice(-6000);
|
|
419
|
+
const prompt = [
|
|
420
|
+
'Below is a conversation between a user ("User") and you, an AI agent ("Agent").',
|
|
421
|
+
'Extract the facts worth remembering LONG-TERM about this conversation: topics the user cares about, decisions made, user preferences, plans/projects mentioned, key conclusions.',
|
|
422
|
+
'',
|
|
423
|
+
'Rules:',
|
|
424
|
+
'- Output 0 to 5 facts, ONE PER LINE, no bullets, no numbering, no preamble.',
|
|
425
|
+
'- Each fact must be self-contained and understandable months later (include subject/app/project names).',
|
|
426
|
+
'- Write facts in Vietnamese.',
|
|
427
|
+
'- Skip small talk, greetings, transient status ("task done", "đang chạy...").',
|
|
428
|
+
'- If nothing is worth remembering, output exactly: NONE',
|
|
429
|
+
'',
|
|
430
|
+
'Conversation:',
|
|
431
|
+
convoText,
|
|
432
|
+
'',
|
|
433
|
+
'Facts:',
|
|
434
|
+
].join('\n');
|
|
435
|
+
const out = await claudeTextOnce(prompt, 'Mem0 Convo', 90000);
|
|
436
|
+
if (!out || /^NONE$/im.test(out.trim()))
|
|
437
|
+
return 0;
|
|
438
|
+
const facts = out.split('\n')
|
|
439
|
+
.map((l) => l.replace(/^[-*\d.\s]+/, '').trim())
|
|
440
|
+
.filter((l) => l.length > 15 && !/^NONE$/i.test(l))
|
|
441
|
+
.slice(0, 5);
|
|
442
|
+
let stored = 0;
|
|
443
|
+
for (const fact of facts) {
|
|
444
|
+
try {
|
|
445
|
+
await callMem0AddMemory(`[Hội thoại] ${fact}`, agentName);
|
|
446
|
+
stored++;
|
|
447
|
+
}
|
|
448
|
+
catch { /* best-effort */ }
|
|
449
|
+
}
|
|
450
|
+
if (stored)
|
|
451
|
+
console.log(`[Mem0 Convo] Stored ${stored} conversation fact(s) for "${agentName}"`);
|
|
452
|
+
return stored;
|
|
453
|
+
}
|
|
403
454
|
/**
|
|
404
455
|
* Build Mem0 MCP server config for an agent.
|
|
405
456
|
* - MEM0_SSH_HOST="local": run mem0-mcp directly (Mem0 infra on same machine)
|