diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index 3c893c38b..3bc7c4372 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -832,7 +832,7 @@ class AgentLoop: if is_subagent and self._persist_subagent_followup(session, msg): self.sessions.save(session) self._set_tool_context(channel, chat_id, msg.metadata.get("message_id")) - history = session.get_history(max_messages=0) + history = session.get_history(max_messages=0, include_timestamps=True) current_role = "assistant" if is_subagent else "user" # Subagent content is already in `history` above; passing it again @@ -901,7 +901,7 @@ class AgentLoop: if isinstance(message_tool, MessageTool): message_tool.start_turn() - history = session.get_history(max_messages=0) + history = session.get_history(max_messages=0, include_timestamps=True) pending_ask_id = pending_ask_user_id(history) if pending_ask_id: diff --git a/nanobot/agent/memory.py b/nanobot/agent/memory.py index 91160d4a5..f11bd6af5 100644 --- a/nanobot/agent/memory.py +++ b/nanobot/agent/memory.py @@ -494,7 +494,7 @@ class Consolidator: session_summary: str | None = None, ) -> tuple[int, str]: """Estimate current prompt size for the normal session history view.""" - history = session.get_history(max_messages=0) + history = session.get_history(max_messages=0, include_timestamps=True) channel, chat_id = (session.key.split(":", 1) if ":" in session.key else (None, None)) probe_messages = self._build_messages( history=history, diff --git a/nanobot/session/manager.py b/nanobot/session/manager.py index ddcfdea14..a94990465 100644 --- a/nanobot/session/manager.py +++ b/nanobot/session/manager.py @@ -30,6 +30,18 @@ class Session: metadata: dict[str, Any] = field(default_factory=dict) last_consolidated: int = 0 # Number of messages already consolidated to files + @staticmethod + def _annotate_message_time(message: dict[str, Any], content: Any) -> Any: + """Expose persisted turn timestamps to the model for relative-date reasoning.""" + timestamp = message.get("timestamp") + if ( + not timestamp + or message.get("role") not in {"user", "assistant"} + or not isinstance(content, str) + ): + return content + return f"[Message Time: {timestamp}]\n{content}" + def add_message(self, role: str, content: str, **kwargs: Any) -> None: """Add a message to the session.""" msg = { @@ -41,7 +53,12 @@ class Session: self.messages.append(msg) self.updated_at = datetime.now() - def get_history(self, max_messages: int = 500) -> list[dict[str, Any]]: + def get_history( + self, + max_messages: int = 500, + *, + include_timestamps: bool = False, + ) -> list[dict[str, Any]]: """Return unconsolidated messages for LLM input, aligned to a legal tool-call boundary.""" unconsolidated = self.messages[self.last_consolidated:] sliced = unconsolidated[-max_messages:] @@ -75,6 +92,8 @@ class Session: image_placeholder_text(p) for p in media if isinstance(p, str) and p ) content = f"{content}\n{breadcrumbs}" if content else breadcrumbs + if include_timestamps: + content = self._annotate_message_time(message, content) entry: dict[str, Any] = {"role": message["role"], "content": content} for key in ("tool_calls", "tool_call_id", "name", "reasoning_content"): if key in message: diff --git a/tests/agent/test_loop_save_turn.py b/tests/agent/test_loop_save_turn.py index 50951824b..a79a6b01a 100644 --- a/tests/agent/test_loop_save_turn.py +++ b/tests/agent/test_loop_save_turn.py @@ -535,7 +535,10 @@ async def test_system_subagent_followup_is_persisted_before_prompt_assembly(tmp_ ) non_system = [m for m in seen["initial_messages"] if m.get("role") != "system"] - assert [m["content"] for m in non_system[:2]] == ["question", "working"] + assert "question" in non_system[0]["content"] + assert "working" in non_system[1]["content"] + assert "[Message Time:" in non_system[0]["content"] + assert "[Message Time:" in non_system[1]["content"] assert non_system[2]["content"].count("subagent result") == 1 assert "Current Time:" in non_system[2]["content"] diff --git a/tests/agent/test_session_manager_history.py b/tests/agent/test_session_manager_history.py index 8b4d0740e..3c2b68e37 100644 --- a/tests/agent/test_session_manager_history.py +++ b/tests/agent/test_session_manager_history.py @@ -194,6 +194,46 @@ def test_get_history_preserves_reasoning_content(): ] +def test_get_history_exposes_turn_timestamps_to_model(): + session = Session(key="test:timestamps") + session.messages.append({ + "role": "user", + "content": "10 点提醒是昨天发生的", + "timestamp": "2026-04-26T22:00:00", + }) + session.messages.append({ + "role": "assistant", + "content": "记下来了", + "timestamp": "2026-04-26T22:00:05", + }) + + history = session.get_history(max_messages=500, include_timestamps=True) + + assert history == [ + { + "role": "user", + "content": "[Message Time: 2026-04-26T22:00:00]\n10 点提醒是昨天发生的", + }, + { + "role": "assistant", + "content": "[Message Time: 2026-04-26T22:00:05]\n记下来了", + }, + ] + + +def test_get_history_does_not_annotate_tool_results_with_timestamps(): + session = Session(key="test:tool-timestamps") + session.messages.append({"role": "user", "content": "run tool"}) + session.messages.extend(_tool_turn("ts", 0)) + session.messages[-1]["timestamp"] = "2026-04-26T22:00:10" + + history = session.get_history(max_messages=500, include_timestamps=True) + + tool_result = history[-1] + assert tool_result["role"] == "tool" + assert tool_result["content"] == "ok" + + # --- Window cuts mid-group: assistant present but some tool results orphaned --- def test_window_cuts_mid_tool_group():