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);
@@ -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);
@@ -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
- * Generate AI-powered reflection from task results using Claude CLI (-p mode).
26
- * Spawns `claude -p <prompt>` locally uses existing Claude subscription, no extra cost.
27
- * Returns a concise lesson learned, or empty string on failure.
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 callMem0Reflect(taskDesc: string, resultSummary: string, status: 'done' | 'failed'): Promise<string>;
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
- export async function callMem0Reflect(taskDesc, resultSummary, status) {
332
- const { execSync } = await import('child_process');
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('[Mem0 Reflect] Claude CLI timed out after 60s');
361
+ console.warn(`[${tag}] Claude CLI timed out after ${timeoutMs / 1000}s`);
379
362
  resolve('');
380
- }, 60000);
363
+ }, timeoutMs);
381
364
  child.on('close', (code) => {
382
365
  clearTimeout(timer);
383
366
  if (code !== 0) {
384
- console.warn(`[Mem0 Reflect] Claude CLI exited ${code}: ${stderr.substring(0, 200)}`);
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
- console.log(`[Mem0 Reflect] Generated: "${reflection.substring(0, 100)}..."`);
394
- resolve(reflection);
371
+ resolve(stdout.trim());
395
372
  });
396
373
  child.on('error', (err) => {
397
374
  clearTimeout(timer);
398
- console.warn(`[Mem0 Reflect] Spawn error: ${err.message}`);
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.181",
3
+ "version": "0.1.182",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"