fix(agent): preserve merged runtime context markers
This commit is contained in:
+49
-3
@@ -19,6 +19,11 @@ from nanobot.agent.context_governance import (
|
||||
from nanobot.agent.hook import AgentHook, AgentHookContext, AgentRunHookContext
|
||||
from nanobot.agent.tools.registry import ToolRegistry, is_tool_error_result
|
||||
from nanobot.providers.base import LLMProvider, LLMResponse, ToolCallRequest
|
||||
from nanobot.runtime_context import (
|
||||
RUNTIME_CONTEXT_MESSAGE_META,
|
||||
detach_runtime_context,
|
||||
reattach_runtime_context,
|
||||
)
|
||||
from nanobot.session.history_visibility import is_hidden_history_message
|
||||
from nanobot.utils.helpers import (
|
||||
IncrementalThinkExtractor,
|
||||
@@ -137,10 +142,51 @@ class AgentRunner:
|
||||
and not is_hidden_history_message(messages[-1])
|
||||
):
|
||||
merged = dict(messages[-1])
|
||||
merged["content"] = cls._merge_message_content(
|
||||
merged.get("content"),
|
||||
injection.get("content"),
|
||||
left_meta = merged.get("_meta")
|
||||
right_meta = injection.get("_meta")
|
||||
left_marker = (
|
||||
left_meta.get(RUNTIME_CONTEXT_MESSAGE_META)
|
||||
if isinstance(left_meta, dict)
|
||||
else None
|
||||
)
|
||||
right_marker = (
|
||||
right_meta.get(RUNTIME_CONTEXT_MESSAGE_META)
|
||||
if isinstance(right_meta, dict)
|
||||
else None
|
||||
)
|
||||
detached_left = (
|
||||
detach_runtime_context(merged.get("content"), left_marker)
|
||||
if isinstance(left_marker, dict)
|
||||
else (merged.get("content"), [], [])
|
||||
)
|
||||
detached_right = (
|
||||
detach_runtime_context(injection.get("content"), right_marker)
|
||||
if isinstance(right_marker, dict)
|
||||
else (injection.get("content"), [], [])
|
||||
)
|
||||
if detached_left is not None and detached_right is not None:
|
||||
left_content, left_sources, left_blocks = detached_left
|
||||
right_content, right_sources, right_blocks = detached_right
|
||||
merged_content = cls._merge_message_content(left_content, right_content)
|
||||
context_blocks = [*left_blocks, *right_blocks]
|
||||
if context_blocks:
|
||||
merged_content, marker = reattach_runtime_context(
|
||||
merged_content,
|
||||
[*left_sources, *right_sources],
|
||||
context_blocks,
|
||||
)
|
||||
internal_meta = dict(left_meta) if isinstance(left_meta, dict) else {}
|
||||
if isinstance(right_meta, dict):
|
||||
for key, value in right_meta.items():
|
||||
internal_meta.setdefault(key, value)
|
||||
internal_meta[RUNTIME_CONTEXT_MESSAGE_META] = marker
|
||||
merged["_meta"] = internal_meta
|
||||
merged["content"] = merged_content
|
||||
else:
|
||||
merged["content"] = cls._merge_message_content(
|
||||
merged.get("content"),
|
||||
injection.get("content"),
|
||||
)
|
||||
messages[-1] = merged
|
||||
continue
|
||||
messages.append(injection)
|
||||
|
||||
@@ -139,6 +139,70 @@ def append_runtime_context(
|
||||
}
|
||||
|
||||
|
||||
def detach_runtime_context(
|
||||
content: Any,
|
||||
marker: Mapping[str, Any],
|
||||
) -> tuple[Any, list[str], list[dict[str, Any]]] | None:
|
||||
"""Detach one validated runtime-context suffix for safe message merging."""
|
||||
if marker.get("version") != 1:
|
||||
return None
|
||||
raw_sources = marker.get("sources")
|
||||
sources = [
|
||||
source
|
||||
for source in raw_sources
|
||||
if isinstance(source, str) and source
|
||||
] if isinstance(raw_sources, list) else []
|
||||
|
||||
suffix = marker.get("suffix")
|
||||
if isinstance(content, str) and isinstance(suffix, str) and suffix:
|
||||
if content == suffix:
|
||||
clean_content = ""
|
||||
elif content.endswith("\n\n" + suffix):
|
||||
clean_content = content[: -(len(suffix) + 2)]
|
||||
else:
|
||||
return None
|
||||
return clean_content, sources, [{"type": "text", "text": suffix}]
|
||||
|
||||
expected = marker.get("blocks")
|
||||
if isinstance(content, list) and isinstance(expected, list) and expected:
|
||||
count = len(expected)
|
||||
if content[-count:] != expected:
|
||||
return None
|
||||
return content[:-count], sources, deepcopy(expected)
|
||||
return None
|
||||
|
||||
|
||||
def reattach_runtime_context(
|
||||
content: Any,
|
||||
sources: Sequence[str],
|
||||
blocks: Sequence[Mapping[str, Any]],
|
||||
) -> tuple[Any, dict[str, Any]]:
|
||||
"""Append detached runtime-context blocks after visible messages are merged."""
|
||||
context_blocks = [deepcopy(dict(block)) for block in blocks]
|
||||
if isinstance(content, str) and all(
|
||||
block.get("type") == "text" and isinstance(block.get("text"), str)
|
||||
for block in context_blocks
|
||||
):
|
||||
suffix = "\n\n".join(block["text"] for block in context_blocks)
|
||||
merged = f"{content}\n\n{suffix}" if content else suffix
|
||||
return merged, {
|
||||
"version": 1,
|
||||
"sources": list(sources),
|
||||
"suffix": suffix,
|
||||
}
|
||||
|
||||
visible_blocks = (
|
||||
[*content]
|
||||
if isinstance(content, list)
|
||||
else ([] if content is None else [{"type": "text", "text": str(content)}])
|
||||
)
|
||||
return [*visible_blocks, *context_blocks], {
|
||||
"version": 1,
|
||||
"sources": list(sources),
|
||||
"blocks": context_blocks,
|
||||
}
|
||||
|
||||
|
||||
def public_history_message(message: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""Return a user-visible copy with trusted runtime context removed exactly."""
|
||||
cleaned = deepcopy(dict(message))
|
||||
|
||||
Reference in New Issue
Block a user