mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 08:13:11 +03:00
refactor(memory): simplify idle consolidation flow
This commit is contained in:
+57
-104
@@ -1000,74 +1000,58 @@ class Consolidator:
|
|||||||
runtime: LLMRuntime,
|
runtime: LLMRuntime,
|
||||||
session_key: str | None = None,
|
session_key: str | None = None,
|
||||||
summary_messages: list[dict[str, Any]] | None = None,
|
summary_messages: list[dict[str, Any]] | None = None,
|
||||||
|
request_messages: list[dict[str, Any]] | None = None,
|
||||||
|
request_tools: list[dict[str, Any]] | None = None,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Summarize messages and append the result to history.jsonl.
|
"""Summarize messages and append the result to history.jsonl.
|
||||||
|
|
||||||
``summary_messages`` adds context but is excluded from raw fallback.
|
``summary_messages`` adds context but is excluded from raw fallback.
|
||||||
|
``request_messages`` preserves a prebuilt model-facing prefix instead
|
||||||
|
of flattening the messages; tools are included but disabled.
|
||||||
"""
|
"""
|
||||||
if not messages:
|
if not messages:
|
||||||
return None
|
return None
|
||||||
messages_to_summarize = public_history_messages(
|
prebuilt_request = request_messages is not None
|
||||||
|
if request_messages is None:
|
||||||
|
formatted = MemoryStore._format_messages(
|
||||||
|
public_history_messages(
|
||||||
summary_messages if summary_messages is not None else messages
|
summary_messages if summary_messages is not None else messages
|
||||||
)
|
)
|
||||||
formatted = MemoryStore._format_messages(messages_to_summarize)
|
|
||||||
formatted = self._truncate_to_token_budget(formatted, runtime=runtime)
|
|
||||||
system_prompt = render_template(
|
|
||||||
"agent/consolidator_archive.md",
|
|
||||||
strip=True,
|
|
||||||
)
|
)
|
||||||
return await self._archive_request(
|
formatted = self._truncate_to_token_budget(formatted, runtime=runtime)
|
||||||
request_messages = [
|
request_messages = [
|
||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": system_prompt,
|
"content": render_template("agent/consolidator_archive.md", strip=True),
|
||||||
},
|
},
|
||||||
{"role": "user", "content": formatted},
|
{"role": "user", "content": formatted},
|
||||||
],
|
]
|
||||||
fallback_messages=messages,
|
|
||||||
runtime=runtime,
|
|
||||||
session_key=session_key,
|
|
||||||
tools=None,
|
|
||||||
tool_choice=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _archive_request(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
request_messages: list[dict[str, Any]],
|
|
||||||
fallback_messages: list[dict[str, Any]],
|
|
||||||
runtime: LLMRuntime,
|
|
||||||
session_key: str | None,
|
|
||||||
tools: list[dict[str, Any]] | None,
|
|
||||||
tool_choice: str | dict[str, Any] | None,
|
|
||||||
) -> str | None:
|
|
||||||
"""Run one archive request and persist either its summary or a raw fallback."""
|
|
||||||
try:
|
try:
|
||||||
response = await runtime.provider.chat_with_retry(
|
response = await runtime.provider.chat_with_retry(
|
||||||
model=runtime.model,
|
model=runtime.model,
|
||||||
messages=request_messages,
|
messages=request_messages,
|
||||||
tools=tools,
|
tools=request_tools if prebuilt_request else None,
|
||||||
tool_choice=tool_choice,
|
tool_choice="none" if prebuilt_request else None,
|
||||||
temperature=runtime.generation.temperature,
|
temperature=runtime.generation.temperature,
|
||||||
max_tokens=runtime.generation.max_tokens,
|
max_tokens=runtime.generation.max_tokens,
|
||||||
reasoning_effort=runtime.generation.reasoning_effort,
|
reasoning_effort=runtime.generation.reasoning_effort,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("Consolidation provider call failed, raw-dumping to history")
|
logger.warning("Consolidation provider call failed, raw-dumping to history")
|
||||||
self.store.raw_archive(fallback_messages, session_key=session_key)
|
self.store.raw_archive(messages, session_key=session_key)
|
||||||
return None
|
return None
|
||||||
if response.finish_reason == "error":
|
if response.finish_reason == "error":
|
||||||
logger.warning("Consolidation provider returned an error, raw-dumping to history")
|
logger.warning("Consolidation provider returned an error, raw-dumping to history")
|
||||||
self.store.raw_archive(fallback_messages, session_key=session_key)
|
self.store.raw_archive(messages, session_key=session_key)
|
||||||
return None
|
return None
|
||||||
if response.has_tool_calls is True:
|
if response.has_tool_calls is True:
|
||||||
logger.warning("Consolidation provider returned tool calls, raw-dumping to history")
|
logger.warning("Consolidation provider returned tool calls, raw-dumping to history")
|
||||||
self.store.raw_archive(fallback_messages, session_key=session_key)
|
self.store.raw_archive(messages, session_key=session_key)
|
||||||
return None
|
return None
|
||||||
summary = response.content
|
summary = response.content
|
||||||
if not summary or not summary.strip():
|
if not summary or not summary.strip():
|
||||||
logger.warning("Consolidation provider returned no summary, raw-dumping to history")
|
logger.warning("Consolidation provider returned no summary, raw-dumping to history")
|
||||||
self.store.raw_archive(fallback_messages, session_key=session_key)
|
self.store.raw_archive(messages, session_key=session_key)
|
||||||
return None
|
return None
|
||||||
self.store.append_history(
|
self.store.append_history(
|
||||||
summary,
|
summary,
|
||||||
@@ -1090,60 +1074,6 @@ class Consolidator:
|
|||||||
timestamp = last_active if isinstance(last_active, str) else session.updated_at.isoformat()
|
timestamp = last_active if isinstance(last_active, str) else session.updated_at.isoformat()
|
||||||
return f"Previous conversation summary (last active {timestamp}):\n{text}"
|
return f"Previous conversation summary (last active {timestamp}):\n{text}"
|
||||||
|
|
||||||
def _build_idle_archive_messages(
|
|
||||||
self,
|
|
||||||
session: Session,
|
|
||||||
messages: list[dict[str, Any]],
|
|
||||||
*,
|
|
||||||
runtime: LLMRuntime,
|
|
||||||
) -> list[dict[str, Any]] | None:
|
|
||||||
"""Append a temporary archive turn to the session's model-facing prefix."""
|
|
||||||
replay_budget = self._input_token_budget(runtime)
|
|
||||||
if replay_budget <= 0:
|
|
||||||
replay_budget = max(128, runtime.context_window_tokens // 2)
|
|
||||||
history = session.get_history(
|
|
||||||
max_messages=replay_max_messages_for_context(runtime.context_window_tokens),
|
|
||||||
max_tokens=replay_budget,
|
|
||||||
)
|
|
||||||
archive_history = Session(
|
|
||||||
key=session.key,
|
|
||||||
messages=messages,
|
|
||||||
).get_history(max_messages=max(1, len(messages)))
|
|
||||||
if (
|
|
||||||
not archive_history
|
|
||||||
or len(history) < len(archive_history)
|
|
||||||
or history[-len(archive_history):] != archive_history
|
|
||||||
):
|
|
||||||
return None
|
|
||||||
prompt = render_template(
|
|
||||||
"agent/consolidator_idle_archive.md",
|
|
||||||
strip=True,
|
|
||||||
archive_count=len(archive_history),
|
|
||||||
)
|
|
||||||
channel = session.key.split(":", 1)[0] if ":" in session.key else None
|
|
||||||
workspace: Path | None = None
|
|
||||||
if self._resolve_prompt_context is not None:
|
|
||||||
channel, workspace = self._resolve_prompt_context(session)
|
|
||||||
built = self._build_messages(
|
|
||||||
history=history,
|
|
||||||
current_message=prompt,
|
|
||||||
channel=channel,
|
|
||||||
session_summary=self._session_summary_for_prompt(session),
|
|
||||||
workspace=workspace,
|
|
||||||
session_key=session.key,
|
|
||||||
unified_session=self.unified_session,
|
|
||||||
)
|
|
||||||
system_prefix = (
|
|
||||||
[built[0]]
|
|
||||||
if built and built[0].get("role") == "system"
|
|
||||||
else []
|
|
||||||
)
|
|
||||||
return [
|
|
||||||
*system_prefix,
|
|
||||||
*history,
|
|
||||||
{"role": "user", "content": prompt},
|
|
||||||
]
|
|
||||||
|
|
||||||
async def _archive_idle_tail(
|
async def _archive_idle_tail(
|
||||||
self,
|
self,
|
||||||
session: Session,
|
session: Session,
|
||||||
@@ -1152,18 +1082,6 @@ class Consolidator:
|
|||||||
runtime: LLMRuntime,
|
runtime: LLMRuntime,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Archive an idle tail by extending the ordinary model-facing messages."""
|
"""Archive an idle tail by extending the ordinary model-facing messages."""
|
||||||
request_messages = self._build_idle_archive_messages(
|
|
||||||
session,
|
|
||||||
messages,
|
|
||||||
runtime=runtime,
|
|
||||||
)
|
|
||||||
if request_messages is None:
|
|
||||||
logger.debug(
|
|
||||||
"Idle consolidation cannot replay the full tail for {}; raw-dumping",
|
|
||||||
session.key,
|
|
||||||
)
|
|
||||||
self.store.raw_archive(messages, session_key=session.key)
|
|
||||||
return None
|
|
||||||
budget = self._input_token_budget(runtime)
|
budget = self._input_token_budget(runtime)
|
||||||
if budget <= 0:
|
if budget <= 0:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -1172,6 +1090,42 @@ class Consolidator:
|
|||||||
)
|
)
|
||||||
self.store.raw_archive(messages, session_key=session.key)
|
self.store.raw_archive(messages, session_key=session.key)
|
||||||
return None
|
return None
|
||||||
|
history = session.get_history(
|
||||||
|
max_messages=replay_max_messages_for_context(runtime.context_window_tokens),
|
||||||
|
max_tokens=budget,
|
||||||
|
)
|
||||||
|
archive_history = Session(
|
||||||
|
key=session.key,
|
||||||
|
messages=messages,
|
||||||
|
).get_history(max_messages=len(messages))
|
||||||
|
if (
|
||||||
|
not archive_history
|
||||||
|
or history[-len(archive_history):] != archive_history
|
||||||
|
):
|
||||||
|
logger.debug(
|
||||||
|
"Idle consolidation cannot replay the full tail for {}; raw-dumping",
|
||||||
|
session.key,
|
||||||
|
)
|
||||||
|
self.store.raw_archive(messages, session_key=session.key)
|
||||||
|
return None
|
||||||
|
prompt = render_template(
|
||||||
|
"agent/consolidator_archive.md",
|
||||||
|
strip=True,
|
||||||
|
archive_count=len(archive_history),
|
||||||
|
)
|
||||||
|
channel = session.key.split(":", 1)[0] if ":" in session.key else None
|
||||||
|
workspace: Path | None = None
|
||||||
|
if self._resolve_prompt_context is not None:
|
||||||
|
channel, workspace = self._resolve_prompt_context(session)
|
||||||
|
request_messages = self._build_messages(
|
||||||
|
history=history,
|
||||||
|
current_message=prompt,
|
||||||
|
channel=channel,
|
||||||
|
session_summary=self._session_summary_for_prompt(session),
|
||||||
|
workspace=workspace,
|
||||||
|
session_key=session.key,
|
||||||
|
unified_session=self.unified_session,
|
||||||
|
)
|
||||||
tools = self._get_tool_definitions()
|
tools = self._get_tool_definitions()
|
||||||
estimated, source = estimate_prompt_tokens_chain(
|
estimated, source = estimate_prompt_tokens_chain(
|
||||||
runtime.provider,
|
runtime.provider,
|
||||||
@@ -1189,13 +1143,12 @@ class Consolidator:
|
|||||||
)
|
)
|
||||||
self.store.raw_archive(messages, session_key=session.key)
|
self.store.raw_archive(messages, session_key=session.key)
|
||||||
return None
|
return None
|
||||||
return await self._archive_request(
|
return await self.archive(
|
||||||
request_messages=request_messages,
|
messages,
|
||||||
fallback_messages=messages,
|
|
||||||
runtime=runtime,
|
runtime=runtime,
|
||||||
session_key=session.key,
|
session_key=session.key,
|
||||||
tools=tools,
|
request_messages=request_messages,
|
||||||
tool_choice="none",
|
request_tools=tools,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def maybe_consolidate_by_tokens(
|
async def maybe_consolidate_by_tokens(
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
|
{% if archive_count is defined %}
|
||||||
|
Create a memory overview for only the final {{ archive_count }} conversation messages immediately before this instruction. Earlier messages are context for resolving references; do not summarize them again.
|
||||||
|
|
||||||
|
Do not call tools. Return only the overview, following these memory rules:
|
||||||
|
{% else %}
|
||||||
Extract key facts from this conversation. For each fact, annotate its memory attributes.
|
Extract key facts from this conversation. For each fact, annotate its memory attributes.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
Only SNIP facts deserve a non-[skip] mark:
|
Only SNIP facts deserve a non-[skip] mark:
|
||||||
- Signal: would the user need to repeat this if forgotten?
|
- Signal: would the user need to repeat this if forgotten?
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
Create a memory overview for only the final {{ archive_count }} conversation messages immediately before this instruction. Earlier messages are context for resolving references; do not summarize them again.
|
|
||||||
|
|
||||||
Do not call tools. Return only the overview, following these memory rules:
|
|
||||||
|
|
||||||
{% include 'agent/consolidator_archive.md' %}
|
|
||||||
@@ -274,6 +274,18 @@ class TestConsolidatorPromptContract:
|
|||||||
assert "check context below" not in prompt.lower()
|
assert "check context below" not in prompt.lower()
|
||||||
assert "Do not mark something [skip] merely because it might already exist" in prompt
|
assert "Do not mark something [skip] merely because it might already exist" in prompt
|
||||||
|
|
||||||
|
def test_archive_prompt_scopes_idle_overview_when_message_count_is_provided(self):
|
||||||
|
prompt = render_template(
|
||||||
|
"agent/consolidator_archive.md",
|
||||||
|
strip=True,
|
||||||
|
archive_count=4,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "only the final 4 conversation messages" in prompt
|
||||||
|
assert "Earlier messages are context" in prompt
|
||||||
|
assert "Do not call tools" in prompt
|
||||||
|
assert "Only SNIP facts" in prompt
|
||||||
|
|
||||||
|
|
||||||
class TestConsolidatorArchiveErrorHandling:
|
class TestConsolidatorArchiveErrorHandling:
|
||||||
"""archive() must fall back to raw_archive when the LLM returns an error
|
"""archive() must fall back to raw_archive when the LLM returns an error
|
||||||
|
|||||||
Reference in New Issue
Block a user