diff --git a/nanobot/agent/context.py b/nanobot/agent/context.py index 19ee935c4..57f00195a 100644 --- a/nanobot/agent/context.py +++ b/nanobot/agent/context.py @@ -157,23 +157,14 @@ class ContextBuilder: ) -> list[dict[str, Any]]: """Build the complete message list for an LLM call.""" extra = goal_state_runtime_lines(session_metadata) - runtime_ctx = self._build_runtime_context( - channel, - chat_id, - self.timezone, + merged = self.build_user_content( + current_message, + media=media, + channel=channel, + chat_id=chat_id, sender_id=sender_id, supplemental_lines=extra or None, ) - user_content = self._build_user_content(current_message, media) - - # Merge runtime context and user content into a single user message - # to avoid consecutive same-role messages that some providers reject. - # Runtime context is appended to keep the user-content prefix stable - # for prompt-cache hits (the context changes every turn due to time). - if isinstance(user_content, str): - merged = f"{user_content}\n\n{runtime_ctx}" - else: - merged = user_content + [{"type": "text", "text": runtime_ctx}] messages = [ {"role": "system", "content": self.build_system_prompt(skill_names, channel=channel, session_summary=session_summary)}, *history, @@ -186,6 +177,25 @@ class ContextBuilder: messages.append({"role": current_role, "content": merged}) return messages + def build_user_content( + self, + text: str, + media: list[str] | None = None, + channel: str | None = None, + chat_id: str | None = None, + sender_id: str | None = None, + supplemental_lines: Sequence[str] | None = None, + ) -> str | list[dict[str, Any]]: + """Build user content with media and runtime context merged into one payload.""" + raw = self._build_user_content(text, media) + runtime_ctx = self._build_runtime_context( + channel, chat_id, self.timezone, + sender_id=sender_id, supplemental_lines=supplemental_lines, + ) + if isinstance(raw, str): + return f"{raw}\n\n{runtime_ctx}" + return raw + [{"type": "text", "text": runtime_ctx}] + def _build_user_content(self, text: str, media: list[str] | None) -> str | list[dict[str, Any]]: """Build user message content with optional base64-encoded images.""" if not media: diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index 0868ebb7c..225a8cd82 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -727,19 +727,15 @@ class AgentLoop: if media: content, media = extract_documents(content, media) media = media or None - user_content = self.context._build_user_content(content, media) extra = goal_state_runtime_lines(session.metadata) if session is not None else [] - runtime_ctx = self.context._build_runtime_context( - pending_msg.channel, - self._runtime_chat_id(pending_msg), - self.context.timezone, + merged = self.context.build_user_content( + content, + media=media, + channel=pending_msg.channel, + chat_id=self._runtime_chat_id(pending_msg), sender_id=pending_msg.sender_id, supplemental_lines=extra or None, ) - if isinstance(user_content, str): - merged: str | list[dict[str, Any]] = f"{user_content}\n\n{runtime_ctx}" - else: - merged = user_content + [{"type": "text", "text": runtime_ctx}] return {"role": "user", "content": merged} items: list[dict[str, Any]] = []