From eeaad6e0c2ffb0e684ee7c19eef7d09dbdf0c447 Mon Sep 17 00:00:00 2001 From: haosenwang1018 Date: Tue, 24 Feb 2026 04:06:22 +0800 Subject: [PATCH 1/4] fix: resolve API key at call time so config changes take effect without restart Previously, WebSearchTool cached the API key in __init__, so keys added to config.json or env vars after gateway startup were never picked up. This caused a confusing 'BRAVE_API_KEY not configured' error even after the key was correctly set (issue #1069). Changes: - Store the init-time key separately, resolve via property at each call - Improve error message to guide users toward the correct fix Closes #1069 --- nanobot/agent/tools/web.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/nanobot/agent/tools/web.py b/nanobot/agent/tools/web.py index 90cdda8..ae69e9e 100644 --- a/nanobot/agent/tools/web.py +++ b/nanobot/agent/tools/web.py @@ -58,12 +58,22 @@ class WebSearchTool(Tool): } def __init__(self, api_key: str | None = None, max_results: int = 5): - self.api_key = api_key or os.environ.get("BRAVE_API_KEY", "") + self._init_api_key = api_key self.max_results = max_results + + @property + def api_key(self) -> str: + """Resolve API key at call time so env/config changes are picked up.""" + return self._init_api_key or os.environ.get("BRAVE_API_KEY", "") async def execute(self, query: str, count: int | None = None, **kwargs: Any) -> str: if not self.api_key: - return "Error: BRAVE_API_KEY not configured" + return ( + "Error: Brave Search API key not configured. " + "Set BRAVE_API_KEY environment variable or add " + "tools.web.search.apiKey to ~/.nanobot/config.json, " + "then restart the gateway." + ) try: n = min(max(count or self.max_results, 1), 10) From a818fff8faaa69e21b331231a23e0730fe78fe40 Mon Sep 17 00:00:00 2001 From: Re-bin Date: Tue, 24 Feb 2026 13:47:17 +0000 Subject: [PATCH 2/4] chore: trim verbose docstrings --- README.md | 2 +- nanobot/agent/tools/registry.py | 14 +------------- nanobot/agent/tools/spawn.py | 7 +------ 3 files changed, 3 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 148c8f4..d2483e4 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ⚡️ Delivers core agent functionality in just **~4,000** lines of code — **99% smaller** than Clawdbot's 430k+ lines. -📏 Real-time line count: **3,897 lines** (run `bash core_agent_lines.sh` to verify anytime) +📏 Real-time line count: **3,955 lines** (run `bash core_agent_lines.sh` to verify anytime) ## 📢 News diff --git a/nanobot/agent/tools/registry.py b/nanobot/agent/tools/registry.py index 8256a59..3af4aef 100644 --- a/nanobot/agent/tools/registry.py +++ b/nanobot/agent/tools/registry.py @@ -36,19 +36,7 @@ class ToolRegistry: return [tool.to_schema() for tool in self._tools.values()] async def execute(self, name: str, params: dict[str, Any]) -> str: - """ - Execute a tool by name with given parameters. - - Args: - name: Tool name. - params: Tool parameters. - - Returns: - Tool execution result as string. - - Raises: - KeyError: If tool not found. - """ + """Execute a tool by name with given parameters.""" _HINT = "\n\n[Analyze the error above and try a different approach.]" tool = self._tools.get(name) diff --git a/nanobot/agent/tools/spawn.py b/nanobot/agent/tools/spawn.py index 5884a07..33cf8e7 100644 --- a/nanobot/agent/tools/spawn.py +++ b/nanobot/agent/tools/spawn.py @@ -9,12 +9,7 @@ if TYPE_CHECKING: class SpawnTool(Tool): - """ - Tool to spawn a subagent for background task execution. - - The subagent runs asynchronously and announces its result back - to the main agent when complete. - """ + """Tool to spawn a subagent for background task execution.""" def __init__(self, manager: "SubagentManager"): self._manager = manager From 56b9b33c6d4dffb9c79bf1fe023fb1444b7b82da Mon Sep 17 00:00:00 2001 From: rickthemad4 Date: Tue, 24 Feb 2026 14:08:38 +0000 Subject: [PATCH 3/4] fix: stabilize system prompt for better cache reuse --- nanobot/agent/context.py | 41 ++++++++++++++----- tests/test_context_prompt_cache.py | 63 ++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 9 deletions(-) create mode 100644 tests/test_context_prompt_cache.py diff --git a/nanobot/agent/context.py b/nanobot/agent/context.py index 98c13f2..b3c4791 100644 --- a/nanobot/agent/context.py +++ b/nanobot/agent/context.py @@ -72,10 +72,6 @@ Skills with available="false" need dependencies installed first - you can try in def _get_identity(self) -> str: """Get the core identity section.""" - from datetime import datetime - import time as _time - now = datetime.now().strftime("%Y-%m-%d %H:%M (%A)") - tz = _time.strftime("%Z") or "UTC" workspace_path = str(self.workspace.expanduser().resolve()) system = platform.system() runtime = f"{'macOS' if system == 'Darwin' else system} {platform.machine()}, Python {platform.python_version()}" @@ -84,9 +80,6 @@ Skills with available="false" need dependencies installed first - you can try in You are nanobot, a helpful AI assistant. -## Current Time -{now} ({tz}) - ## Runtime {runtime} @@ -108,6 +101,34 @@ Reply directly with text for conversations. Only use the 'message' tool to send ## Memory - Remember important facts: write to {workspace_path}/memory/MEMORY.md - Recall past events: grep {workspace_path}/memory/HISTORY.md""" + + @staticmethod + def _build_runtime_context(channel: str | None, chat_id: str | None) -> str: + """Build dynamic runtime context and attach it to the tail user message.""" + from datetime import datetime + import time as _time + + now = datetime.now().strftime("%Y-%m-%d %H:%M (%A)") + tz = _time.strftime("%Z") or "UTC" + lines = [f"Current Time: {now} ({tz})"] + if channel and chat_id: + lines.append(f"Channel: {channel}") + lines.append(f"Chat ID: {chat_id}") + return "\n".join(lines) + + @staticmethod + def _append_runtime_context( + user_content: str | list[dict[str, Any]], + runtime_context: str, + ) -> str | list[dict[str, Any]]: + """Append runtime context at the tail of the user message.""" + runtime_block = f"[Runtime Context]\n{runtime_context}" + if isinstance(user_content, str): + return f"{user_content}\n\n{runtime_block}" + + content = list(user_content) + content.append({"type": "text", "text": runtime_block}) + return content def _load_bootstrap_files(self) -> str: """Load all bootstrap files from workspace.""" @@ -148,8 +169,6 @@ Reply directly with text for conversations. Only use the 'message' tool to send # System prompt system_prompt = self.build_system_prompt(skill_names) - if channel and chat_id: - system_prompt += f"\n\n## Current Session\nChannel: {channel}\nChat ID: {chat_id}" messages.append({"role": "system", "content": system_prompt}) # History @@ -157,6 +176,10 @@ Reply directly with text for conversations. Only use the 'message' tool to send # Current message (with optional image attachments) user_content = self._build_user_content(current_message, media) + user_content = self._append_runtime_context( + user_content=user_content, + runtime_context=self._build_runtime_context(channel, chat_id), + ) messages.append({"role": "user", "content": user_content}) return messages diff --git a/tests/test_context_prompt_cache.py b/tests/test_context_prompt_cache.py new file mode 100644 index 0000000..8e2333c --- /dev/null +++ b/tests/test_context_prompt_cache.py @@ -0,0 +1,63 @@ +"""Tests for cache-friendly prompt construction.""" + +from __future__ import annotations + +from datetime import datetime as real_datetime +from pathlib import Path +import datetime as datetime_module + +from nanobot.agent.context import ContextBuilder + + +class _FakeDatetime(real_datetime): + current = real_datetime(2026, 2, 24, 13, 59) + + @classmethod + def now(cls, tz=None): # type: ignore[override] + return cls.current + + +def _make_workspace(tmp_path: Path) -> Path: + workspace = tmp_path / "workspace" + workspace.mkdir(parents=True) + return workspace + + +def test_system_prompt_stays_stable_when_clock_changes(tmp_path, monkeypatch) -> None: + """System prompt should not change just because wall clock minute changes.""" + monkeypatch.setattr(datetime_module, "datetime", _FakeDatetime) + + workspace = _make_workspace(tmp_path) + builder = ContextBuilder(workspace) + + _FakeDatetime.current = real_datetime(2026, 2, 24, 13, 59) + prompt1 = builder.build_system_prompt() + + _FakeDatetime.current = real_datetime(2026, 2, 24, 14, 0) + prompt2 = builder.build_system_prompt() + + assert prompt1 == prompt2 + + +def test_runtime_context_is_appended_to_current_user_message(tmp_path) -> None: + """Dynamic runtime details should be added at the tail user message, not system.""" + workspace = _make_workspace(tmp_path) + builder = ContextBuilder(workspace) + + messages = builder.build_messages( + history=[], + current_message="Return exactly: OK", + channel="cli", + chat_id="direct", + ) + + assert messages[0]["role"] == "system" + assert "## Current Session" not in messages[0]["content"] + + assert messages[-1]["role"] == "user" + user_content = messages[-1]["content"] + assert isinstance(user_content, str) + assert "Return exactly: OK" in user_content + assert "Current Time:" in user_content + assert "Channel: cli" in user_content + assert "Chat ID: direct" in user_content From f294e9d065845b799fc201792da9e61b83d56eb5 Mon Sep 17 00:00:00 2001 From: Re-bin Date: Tue, 24 Feb 2026 16:15:21 +0000 Subject: [PATCH 4/4] refactor: merge runtime context helpers and move imports to top --- nanobot/agent/context.py | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/nanobot/agent/context.py b/nanobot/agent/context.py index b3c4791..088d4c5 100644 --- a/nanobot/agent/context.py +++ b/nanobot/agent/context.py @@ -3,6 +3,8 @@ import base64 import mimetypes import platform +import time +from datetime import datetime from pathlib import Path from typing import Any @@ -103,32 +105,21 @@ Reply directly with text for conversations. Only use the 'message' tool to send - Recall past events: grep {workspace_path}/memory/HISTORY.md""" @staticmethod - def _build_runtime_context(channel: str | None, chat_id: str | None) -> str: - """Build dynamic runtime context and attach it to the tail user message.""" - from datetime import datetime - import time as _time - + def _inject_runtime_context( + user_content: str | list[dict[str, Any]], + channel: str | None, + chat_id: str | None, + ) -> str | list[dict[str, Any]]: + """Append dynamic runtime context to the tail of the user message.""" now = datetime.now().strftime("%Y-%m-%d %H:%M (%A)") - tz = _time.strftime("%Z") or "UTC" + tz = time.strftime("%Z") or "UTC" lines = [f"Current Time: {now} ({tz})"] if channel and chat_id: - lines.append(f"Channel: {channel}") - lines.append(f"Chat ID: {chat_id}") - return "\n".join(lines) - - @staticmethod - def _append_runtime_context( - user_content: str | list[dict[str, Any]], - runtime_context: str, - ) -> str | list[dict[str, Any]]: - """Append runtime context at the tail of the user message.""" - runtime_block = f"[Runtime Context]\n{runtime_context}" + lines += [f"Channel: {channel}", f"Chat ID: {chat_id}"] + block = "[Runtime Context]\n" + "\n".join(lines) if isinstance(user_content, str): - return f"{user_content}\n\n{runtime_block}" - - content = list(user_content) - content.append({"type": "text", "text": runtime_block}) - return content + return f"{user_content}\n\n{block}" + return [*user_content, {"type": "text", "text": block}] def _load_bootstrap_files(self) -> str: """Load all bootstrap files from workspace.""" @@ -176,10 +167,7 @@ Reply directly with text for conversations. Only use the 'message' tool to send # Current message (with optional image attachments) user_content = self._build_user_content(current_message, media) - user_content = self._append_runtime_context( - user_content=user_content, - runtime_context=self._build_runtime_context(channel, chat_id), - ) + user_content = self._inject_runtime_context(user_content, channel, chat_id) messages.append({"role": "user", "content": user_content}) return messages