Fix chat memory persistence, duplicate tool calls, and heartbeat log noise
- Agent: deduplicate tool calls across iterations, reduce max iterations 10→5, add system prompt instructions to prevent re-calling tools - Router: preserve tool name in history messages, add anti-loop and delegate_task instructions to system prompt - Memory: auto_flush now deletes flushed messages from DB so conversations don't get re-summarized repeatedly, skip tool results in summaries - DB: add delete_messages() method, include message id in get_messages() - Scheduler: stop logging routine heartbeat checks to daily log Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>cora-start
parent
af767f9684
commit
1866d48cb2
|
|
@ -14,7 +14,7 @@ from .router import build_system_prompt, format_messages_for_llm
|
|||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MAX_TOOL_ITERATIONS = 10
|
||||
MAX_TOOL_ITERATIONS = 5
|
||||
|
||||
|
||||
class Agent:
|
||||
|
|
@ -72,6 +72,8 @@ class Agent:
|
|||
messages = format_messages_for_llm(system_prompt, history, self.config.memory.max_context_messages)
|
||||
|
||||
# Agent loop: LLM call → tool execution → repeat
|
||||
seen_tool_calls: set[str] = set() # track (name, args_json) to prevent duplicates
|
||||
|
||||
for iteration in range(MAX_TOOL_ITERATIONS):
|
||||
full_response = ""
|
||||
tool_calls = []
|
||||
|
|
@ -89,10 +91,28 @@ class Agent:
|
|||
self.db.add_message(conv_id, "assistant", full_response, model=self.llm.current_model)
|
||||
break
|
||||
|
||||
# Filter out duplicate tool calls
|
||||
unique_tool_calls = []
|
||||
for tc in tool_calls:
|
||||
call_key = f"{tc['name']}:{json.dumps(tc.get('input', {}), sort_keys=True)}"
|
||||
if call_key in seen_tool_calls:
|
||||
log.warning("Skipping duplicate tool call: %s", tc["name"])
|
||||
continue
|
||||
seen_tool_calls.add(call_key)
|
||||
unique_tool_calls.append(tc)
|
||||
|
||||
if not unique_tool_calls:
|
||||
# All tool calls were duplicates — force the model to respond
|
||||
if full_response:
|
||||
self.db.add_message(conv_id, "assistant", full_response, model=self.llm.current_model)
|
||||
else:
|
||||
yield "(I already have the information needed to answer.)"
|
||||
break
|
||||
|
||||
# Store assistant message with tool calls
|
||||
self.db.add_message(
|
||||
conv_id, "assistant", full_response,
|
||||
tool_calls=[{"name": tc["name"], "input": tc["input"]} for tc in tool_calls],
|
||||
tool_calls=[{"name": tc["name"], "input": tc["input"]} for tc in unique_tool_calls],
|
||||
model=self.llm.current_model,
|
||||
)
|
||||
|
||||
|
|
@ -100,8 +120,8 @@ class Agent:
|
|||
if self._tools:
|
||||
messages.append({"role": "assistant", "content": full_response or "I'll use some tools to help with that."})
|
||||
|
||||
for tc in tool_calls:
|
||||
yield f"\n\n🔧 **Using tool: {tc['name']}**\n"
|
||||
for tc in unique_tool_calls:
|
||||
yield f"\n\n**Using tool: {tc['name']}**\n"
|
||||
try:
|
||||
result = self._tools.execute(tc["name"], tc.get("input", {}))
|
||||
except Exception as e:
|
||||
|
|
@ -114,7 +134,7 @@ class Agent:
|
|||
# No tool system configured - just mention tool was requested
|
||||
if full_response:
|
||||
self.db.add_message(conv_id, "assistant", full_response, model=self.llm.current_model)
|
||||
for tc in tool_calls:
|
||||
for tc in unique_tool_calls:
|
||||
yield f"\n(Tool requested: {tc['name']} - tool system not yet initialized)\n"
|
||||
break
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ class Database:
|
|||
|
||||
def get_messages(self, conv_id: str, limit: int = 100) -> list[dict]:
|
||||
rows = self._conn.execute(
|
||||
"""SELECT role, content, tool_calls, tool_result, model, created_at
|
||||
"""SELECT id, role, content, tool_calls, tool_result, model, created_at
|
||||
FROM messages WHERE conv_id = ? ORDER BY created_at ASC LIMIT ?""",
|
||||
(conv_id, limit),
|
||||
).fetchall()
|
||||
|
|
@ -137,6 +137,16 @@ class Database:
|
|||
).fetchone()
|
||||
return row["cnt"]
|
||||
|
||||
def delete_messages(self, message_ids: list[int]):
|
||||
"""Delete messages by their IDs (used by auto_flush)."""
|
||||
if not message_ids:
|
||||
return
|
||||
placeholders = ",".join("?" for _ in message_ids)
|
||||
self._conn.execute(
|
||||
f"DELETE FROM messages WHERE id IN ({placeholders})", message_ids
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
# -- Scheduled Tasks --
|
||||
|
||||
def add_scheduled_task(self, name: str, prompt: str, schedule: str) -> int:
|
||||
|
|
|
|||
|
|
@ -99,21 +99,37 @@ class MemorySystem:
|
|||
return self._vector_search(query_vec, top_k)
|
||||
|
||||
def auto_flush(self, conv_id: str):
|
||||
"""Summarize old messages and move to daily log."""
|
||||
"""Summarize old messages and move to daily log, then delete flushed messages."""
|
||||
messages = self.db.get_messages(conv_id, limit=200)
|
||||
if len(messages) < self.config.memory.flush_threshold:
|
||||
return
|
||||
|
||||
# Take older messages for summarization
|
||||
to_summarize = messages[:-10] # keep last 10 in context
|
||||
text_block = "\n".join(
|
||||
f"{m['role']}: {m['content'][:200]}" for m in to_summarize
|
||||
if m.get("content")
|
||||
)
|
||||
# Take older messages for summarization, keep last 10 in context
|
||||
to_summarize = messages[:-10]
|
||||
if not to_summarize:
|
||||
return
|
||||
|
||||
summary = f"Conversation summary ({len(to_summarize)} messages): {text_block[:1000]}"
|
||||
# Build a concise summary (skip tool results, keep user/assistant text)
|
||||
summary_parts = []
|
||||
for m in to_summarize:
|
||||
role = m.get("role", "")
|
||||
content = (m.get("content") or "").strip()
|
||||
if not content or role == "tool":
|
||||
continue
|
||||
summary_parts.append(f"{role}: {content[:150]}")
|
||||
|
||||
if not summary_parts:
|
||||
return
|
||||
|
||||
summary = f"Conversation summary ({len(to_summarize)} messages):\n" + "\n".join(summary_parts[:20])
|
||||
self.log_daily(summary)
|
||||
log.info("Auto-flushed %d messages to daily log", len(to_summarize))
|
||||
|
||||
# Delete the flushed messages from DB so they don't get re-flushed
|
||||
flushed_ids = [m["id"] for m in to_summarize if "id" in m]
|
||||
if flushed_ids:
|
||||
self.db.delete_messages(flushed_ids)
|
||||
|
||||
log.info("Auto-flushed %d messages from conv %s", len(to_summarize), conv_id)
|
||||
|
||||
def reindex_all(self):
|
||||
"""Rebuild the embedding index from all memory files."""
|
||||
|
|
|
|||
|
|
@ -38,7 +38,13 @@ def build_system_prompt(
|
|||
"- If you learn something important about the user, save it to memory.\n"
|
||||
"- Be concise but thorough. Don't pad responses unnecessarily.\n"
|
||||
"- When uncertain, ask for clarification.\n"
|
||||
"- Reference memories naturally when relevant."
|
||||
"- Reference memories naturally when relevant.\n"
|
||||
"- IMPORTANT: Do NOT call the same tool twice with the same arguments. "
|
||||
"If a tool already returned a result, use that result — do not re-call it.\n"
|
||||
"- After using tools, always respond to the user with a final answer. "
|
||||
"Do not end your turn with only tool calls and no text.\n"
|
||||
"- For tasks requiring shell commands, file edits, or system access, "
|
||||
"use the delegate_task tool instead of trying to do it yourself."
|
||||
)
|
||||
|
||||
return "\n\n---\n\n".join(parts)
|
||||
|
|
@ -62,6 +68,7 @@ def format_messages_for_llm(
|
|||
messages.append({"role": role, "content": content})
|
||||
elif role == "tool":
|
||||
# Tool results go as a user message with context
|
||||
messages.append({"role": "user", "content": f"[Tool Result]\n{content}"})
|
||||
tool_name = msg.get("tool_result", "unknown")
|
||||
messages.append({"role": "user", "content": f'[Tool "{tool_name}" result]\n{content}'})
|
||||
|
||||
return messages
|
||||
|
|
|
|||
|
|
@ -113,6 +113,3 @@ class Scheduler:
|
|||
log.debug("Heartbeat: all clear")
|
||||
else:
|
||||
log.info("Heartbeat action taken: %s", result[:200])
|
||||
# Log to daily log
|
||||
if self.agent._memory:
|
||||
self.agent._memory.log_daily(f"[Heartbeat] {result[:500]}")
|
||||
|
|
|
|||
Loading…
Reference in New Issue