Merge pull request #866: refactor(memory): use tool call instead of JSON text for memory consolidation

This commit is contained in:
Re-bin
2026-02-21 08:02:03 +00:00

View File

@@ -9,7 +9,6 @@ from contextlib import AsyncExitStack
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Awaitable, Callable from typing import TYPE_CHECKING, Awaitable, Callable
import json_repair
from loguru import logger from loguru import logger
from nanobot.agent.context import ContextBuilder from nanobot.agent.context import ContextBuilder
@@ -479,54 +478,58 @@ class AgentLoop:
conversation = "\n".join(lines) conversation = "\n".join(lines)
current_memory = memory.read_long_term() current_memory = memory.read_long_term()
prompt = f"""You are a memory consolidation agent. Process this conversation and return a JSON object with exactly two keys: prompt = f"""Process this conversation and call the save_memory tool with your consolidation.
1. "history_entry": A paragraph (2-5 sentences) summarizing the key events/decisions/topics. Start with a timestamp like [YYYY-MM-DD HH:MM]. Include enough detail to be useful when found by grep search later.
2. "memory_update": The updated long-term memory content. Add any new facts: user location, preferences, personal info, habits, project context, technical decisions, tools/services used. If nothing new, return the existing content unchanged.
## Current Long-term Memory ## Current Long-term Memory
{current_memory or "(empty)"} {current_memory or "(empty)"}
## Conversation to Process ## Conversation to Process
{conversation} {conversation}"""
**IMPORTANT**: Both values MUST be strings, not objects or arrays. save_memory_tool = [
{
Example: "type": "function",
{{ "function": {
"history_entry": "[2026-02-14 22:50] User asked about...", "name": "save_memory",
"memory_update": "- Host: HARRYBOOK-T14P\n- Name: Nado" "description": "Save the memory consolidation result to persistent storage.",
}} "parameters": {
"type": "object",
Respond with ONLY valid JSON, no markdown fences.""" "properties": {
"history_entry": {
"type": "string",
"description": "A paragraph (2-5 sentences) summarizing key events/decisions/topics. Start with a timestamp like [YYYY-MM-DD HH:MM]. Include enough detail to be useful when found by grep search later.",
},
"memory_update": {
"type": "string",
"description": "The full updated long-term memory content as a markdown string. Include all existing facts plus any new facts: user location, preferences, personal info, habits, project context, technical decisions, tools/services used. If nothing new, return the existing content unchanged.",
},
},
"required": ["history_entry", "memory_update"],
},
},
}
]
try: try:
response = await self.provider.chat( response = await self.provider.chat(
messages=[ messages=[
{"role": "system", "content": "You are a memory consolidation agent. Respond only with valid JSON."}, {"role": "system", "content": "You are a memory consolidation agent. Call the save_memory tool with your consolidation of the conversation."},
{"role": "user", "content": prompt}, {"role": "user", "content": prompt},
], ],
tools=save_memory_tool,
model=self.model, model=self.model,
) )
text = (response.content or "").strip()
if not text: if not response.has_tool_calls:
logger.warning("Memory consolidation: LLM returned empty response, skipping") logger.warning("Memory consolidation: LLM did not call save_memory tool, skipping")
return
if text.startswith("```"):
text = text.split("\n", 1)[-1].rsplit("```", 1)[0].strip()
result = json_repair.loads(text)
if not isinstance(result, dict):
logger.warning("Memory consolidation: unexpected response type, skipping. Response: {}", text[:200])
return return
if entry := result.get("history_entry"): args = response.tool_calls[0].arguments
# Defensive: ensure entry is a string (LLM may return dict) if entry := args.get("history_entry"):
if not isinstance(entry, str): if not isinstance(entry, str):
entry = json.dumps(entry, ensure_ascii=False) entry = json.dumps(entry, ensure_ascii=False)
memory.append_history(entry) memory.append_history(entry)
if update := result.get("memory_update"): if update := args.get("memory_update"):
# Defensive: ensure update is a string
if not isinstance(update, str): if not isinstance(update, str):
update = json.dumps(update, ensure_ascii=False) update = json.dumps(update, ensure_ascii=False)
if update != current_memory: if update != current_memory: