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__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
MAX_TOOL_ITERATIONS = 10
|
MAX_TOOL_ITERATIONS = 5
|
||||||
|
|
||||||
|
|
||||||
class Agent:
|
class Agent:
|
||||||
|
|
@ -72,6 +72,8 @@ class Agent:
|
||||||
messages = format_messages_for_llm(system_prompt, history, self.config.memory.max_context_messages)
|
messages = format_messages_for_llm(system_prompt, history, self.config.memory.max_context_messages)
|
||||||
|
|
||||||
# Agent loop: LLM call → tool execution → repeat
|
# 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):
|
for iteration in range(MAX_TOOL_ITERATIONS):
|
||||||
full_response = ""
|
full_response = ""
|
||||||
tool_calls = []
|
tool_calls = []
|
||||||
|
|
@ -89,10 +91,28 @@ class Agent:
|
||||||
self.db.add_message(conv_id, "assistant", full_response, model=self.llm.current_model)
|
self.db.add_message(conv_id, "assistant", full_response, model=self.llm.current_model)
|
||||||
break
|
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
|
# Store assistant message with tool calls
|
||||||
self.db.add_message(
|
self.db.add_message(
|
||||||
conv_id, "assistant", full_response,
|
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,
|
model=self.llm.current_model,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -100,8 +120,8 @@ class Agent:
|
||||||
if self._tools:
|
if self._tools:
|
||||||
messages.append({"role": "assistant", "content": full_response or "I'll use some tools to help with that."})
|
messages.append({"role": "assistant", "content": full_response or "I'll use some tools to help with that."})
|
||||||
|
|
||||||
for tc in tool_calls:
|
for tc in unique_tool_calls:
|
||||||
yield f"\n\n🔧 **Using tool: {tc['name']}**\n"
|
yield f"\n\n**Using tool: {tc['name']}**\n"
|
||||||
try:
|
try:
|
||||||
result = self._tools.execute(tc["name"], tc.get("input", {}))
|
result = self._tools.execute(tc["name"], tc.get("input", {}))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -114,7 +134,7 @@ class Agent:
|
||||||
# No tool system configured - just mention tool was requested
|
# No tool system configured - just mention tool was requested
|
||||||
if full_response:
|
if full_response:
|
||||||
self.db.add_message(conv_id, "assistant", full_response, model=self.llm.current_model)
|
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"
|
yield f"\n(Tool requested: {tc['name']} - tool system not yet initialized)\n"
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ class Database:
|
||||||
|
|
||||||
def get_messages(self, conv_id: str, limit: int = 100) -> list[dict]:
|
def get_messages(self, conv_id: str, limit: int = 100) -> list[dict]:
|
||||||
rows = self._conn.execute(
|
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 ?""",
|
FROM messages WHERE conv_id = ? ORDER BY created_at ASC LIMIT ?""",
|
||||||
(conv_id, limit),
|
(conv_id, limit),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
@ -137,6 +137,16 @@ class Database:
|
||||||
).fetchone()
|
).fetchone()
|
||||||
return row["cnt"]
|
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 --
|
# -- Scheduled Tasks --
|
||||||
|
|
||||||
def add_scheduled_task(self, name: str, prompt: str, schedule: str) -> int:
|
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)
|
return self._vector_search(query_vec, top_k)
|
||||||
|
|
||||||
def auto_flush(self, conv_id: str):
|
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)
|
messages = self.db.get_messages(conv_id, limit=200)
|
||||||
if len(messages) < self.config.memory.flush_threshold:
|
if len(messages) < self.config.memory.flush_threshold:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Take older messages for summarization
|
# Take older messages for summarization, keep last 10 in context
|
||||||
to_summarize = messages[:-10] # keep last 10 in context
|
to_summarize = messages[:-10]
|
||||||
text_block = "\n".join(
|
if not to_summarize:
|
||||||
f"{m['role']}: {m['content'][:200]}" for m in to_summarize
|
return
|
||||||
if m.get("content")
|
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
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):
|
def reindex_all(self):
|
||||||
"""Rebuild the embedding index from all memory files."""
|
"""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"
|
"- If you learn something important about the user, save it to memory.\n"
|
||||||
"- Be concise but thorough. Don't pad responses unnecessarily.\n"
|
"- Be concise but thorough. Don't pad responses unnecessarily.\n"
|
||||||
"- When uncertain, ask for clarification.\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)
|
return "\n\n---\n\n".join(parts)
|
||||||
|
|
@ -62,6 +68,7 @@ def format_messages_for_llm(
|
||||||
messages.append({"role": role, "content": content})
|
messages.append({"role": role, "content": content})
|
||||||
elif role == "tool":
|
elif role == "tool":
|
||||||
# Tool results go as a user message with context
|
# 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
|
return messages
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,3 @@ class Scheduler:
|
||||||
log.debug("Heartbeat: all clear")
|
log.debug("Heartbeat: all clear")
|
||||||
else:
|
else:
|
||||||
log.info("Heartbeat action taken: %s", result[:200])
|
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