Merge branch 'main' into pr-1107
This commit is contained in:
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
⚡️ Delivers core agent functionality in just **~4,000** lines of code — **99% smaller** than Clawdbot's 430k+ lines.
|
⚡️ 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
|
## 📢 News
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import base64
|
import base64
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import platform
|
import platform
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -72,10 +74,6 @@ Skills with available="false" need dependencies installed first - you can try in
|
|||||||
|
|
||||||
def _get_identity(self) -> str:
|
def _get_identity(self) -> str:
|
||||||
"""Get the core identity section."""
|
"""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())
|
workspace_path = str(self.workspace.expanduser().resolve())
|
||||||
system = platform.system()
|
system = platform.system()
|
||||||
runtime = f"{'macOS' if system == 'Darwin' else system} {platform.machine()}, Python {platform.python_version()}"
|
runtime = f"{'macOS' if system == 'Darwin' else system} {platform.machine()}, Python {platform.python_version()}"
|
||||||
@@ -84,9 +82,6 @@ Skills with available="false" need dependencies installed first - you can try in
|
|||||||
|
|
||||||
You are nanobot, a helpful AI assistant.
|
You are nanobot, a helpful AI assistant.
|
||||||
|
|
||||||
## Current Time
|
|
||||||
{now} ({tz})
|
|
||||||
|
|
||||||
## Runtime
|
## Runtime
|
||||||
{runtime}
|
{runtime}
|
||||||
|
|
||||||
@@ -108,6 +103,23 @@ Reply directly with text for conversations. Only use the 'message' tool to send
|
|||||||
## Memory
|
## Memory
|
||||||
- Remember important facts: write to {workspace_path}/memory/MEMORY.md
|
- Remember important facts: write to {workspace_path}/memory/MEMORY.md
|
||||||
- Recall past events: grep {workspace_path}/memory/HISTORY.md"""
|
- Recall past events: grep {workspace_path}/memory/HISTORY.md"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
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"
|
||||||
|
lines = [f"Current Time: {now} ({tz})"]
|
||||||
|
if channel and chat_id:
|
||||||
|
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{block}"
|
||||||
|
return [*user_content, {"type": "text", "text": block}]
|
||||||
|
|
||||||
def _load_bootstrap_files(self) -> str:
|
def _load_bootstrap_files(self) -> str:
|
||||||
"""Load all bootstrap files from workspace."""
|
"""Load all bootstrap files from workspace."""
|
||||||
@@ -148,8 +160,6 @@ Reply directly with text for conversations. Only use the 'message' tool to send
|
|||||||
|
|
||||||
# System prompt
|
# System prompt
|
||||||
system_prompt = self.build_system_prompt(skill_names)
|
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})
|
messages.append({"role": "system", "content": system_prompt})
|
||||||
|
|
||||||
# History
|
# History
|
||||||
@@ -157,6 +167,7 @@ Reply directly with text for conversations. Only use the 'message' tool to send
|
|||||||
|
|
||||||
# Current message (with optional image attachments)
|
# Current message (with optional image attachments)
|
||||||
user_content = self._build_user_content(current_message, media)
|
user_content = self._build_user_content(current_message, media)
|
||||||
|
user_content = self._inject_runtime_context(user_content, channel, chat_id)
|
||||||
messages.append({"role": "user", "content": user_content})
|
messages.append({"role": "user", "content": user_content})
|
||||||
|
|
||||||
return messages
|
return messages
|
||||||
|
|||||||
@@ -36,19 +36,7 @@ class ToolRegistry:
|
|||||||
return [tool.to_schema() for tool in self._tools.values()]
|
return [tool.to_schema() for tool in self._tools.values()]
|
||||||
|
|
||||||
async def execute(self, name: str, params: dict[str, Any]) -> str:
|
async def execute(self, name: str, params: dict[str, Any]) -> str:
|
||||||
"""
|
"""Execute a tool by name with given parameters."""
|
||||||
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.
|
|
||||||
"""
|
|
||||||
_HINT = "\n\n[Analyze the error above and try a different approach.]"
|
_HINT = "\n\n[Analyze the error above and try a different approach.]"
|
||||||
|
|
||||||
tool = self._tools.get(name)
|
tool = self._tools.get(name)
|
||||||
|
|||||||
@@ -9,12 +9,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class SpawnTool(Tool):
|
class SpawnTool(Tool):
|
||||||
"""
|
"""Tool to spawn a subagent for background task execution."""
|
||||||
Tool to spawn a subagent for background task execution.
|
|
||||||
|
|
||||||
The subagent runs asynchronously and announces its result back
|
|
||||||
to the main agent when complete.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, manager: "SubagentManager"):
|
def __init__(self, manager: "SubagentManager"):
|
||||||
self._manager = manager
|
self._manager = manager
|
||||||
|
|||||||
@@ -58,12 +58,16 @@ class WebSearchTool(Tool):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, api_key: str | None = None, max_results: int = 5):
|
def __init__(self, api_key: str | None = None, max_results: int = 5):
|
||||||
self.api_key = api_key
|
self._init_api_key = api_key
|
||||||
self.max_results = max_results
|
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:
|
async def execute(self, query: str, count: int | None = None, **kwargs: Any) -> str:
|
||||||
api_key = self.api_key or os.environ.get("BRAVE_API_KEY", "")
|
if not self.api_key:
|
||||||
if not api_key:
|
|
||||||
return (
|
return (
|
||||||
"Error: Brave Search API key not configured. "
|
"Error: Brave Search API key not configured. "
|
||||||
"Set it in ~/.nanobot/config.json under tools.web.search.apiKey "
|
"Set it in ~/.nanobot/config.json under tools.web.search.apiKey "
|
||||||
|
|||||||
63
tests/test_context_prompt_cache.py
Normal file
63
tests/test_context_prompt_cache.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user