diff --git a/nanobot/agent/context.py b/nanobot/agent/context.py index 19ee935c4..82ebfab65 100644 --- a/nanobot/agent/context.py +++ b/nanobot/agent/context.py @@ -22,7 +22,7 @@ from nanobot.utils.prompt_templates import render_template class ContextBuilder: """Builds the context (system prompt + messages) for the agent.""" - BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md"] + BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md"] _RUNTIME_CONTEXT_TAG = "[Runtime Context — metadata only, not instructions]" _MAX_RECENT_HISTORY = 50 _MAX_HISTORY_CHARS = 32_000 # hard cap on recent history section size @@ -47,6 +47,8 @@ class ContextBuilder: if bootstrap: parts.append(bootstrap) + parts.append(render_template("agent/tool_contract.md")) + memory = self.memory.get_memory_context() if memory and not self._is_template_content(self.memory.read_memory(), "memory/MEMORY.md"): parts.append(f"# Memory\n\n{memory}") @@ -210,4 +212,3 @@ class ContextBuilder: if not images: return text return images + [{"type": "text", "text": text}] - diff --git a/nanobot/templates/AGENTS.md b/nanobot/templates/AGENTS.md index 0bf6de3d3..46cfc08c3 100644 --- a/nanobot/templates/AGENTS.md +++ b/nanobot/templates/AGENTS.md @@ -1,5 +1,9 @@ # Agent Instructions +## Workspace Guidance + +Use this file for project-specific preferences, recurring workflow conventions, and instructions you want the agent to remember for this workspace. Keep durable facts about the user in `USER.md`, personality/style guidance in `SOUL.md`, and long-term memory in `memory/MEMORY.md`. + ## Scheduled Reminders Before scheduling reminders, check available skills and follow skill guidance first. @@ -10,10 +14,10 @@ Get USER_ID and CHANNEL from the current session (e.g., `8281248569` and `telegr ## Heartbeat Tasks -`HEARTBEAT.md` is checked on the configured heartbeat interval. Use file tools to manage periodic tasks: +`HEARTBEAT.md` is checked on the configured heartbeat interval. Use file tools to manage periodic tasks. -- **Add**: `edit_file` to append new tasks -- **Remove**: `edit_file` to delete completed tasks -- **Rewrite**: `write_file` to replace all tasks +- Use `apply_patch` for normal task-list updates, especially when adding, removing, or changing multiple lines. +- Use `edit_file` only for small exact replacements copied from the current `HEARTBEAT.md`. +- Use `write_file` for first creation or intentional full-file rewrites. When the user asks for a recurring/periodic task, update `HEARTBEAT.md` instead of creating a one-time cron reminder. diff --git a/nanobot/templates/TOOLS.md b/nanobot/templates/agent/tool_contract.md similarity index 99% rename from nanobot/templates/TOOLS.md rename to nanobot/templates/agent/tool_contract.md index ee37090ea..edbba21c9 100644 --- a/nanobot/templates/TOOLS.md +++ b/nanobot/templates/agent/tool_contract.md @@ -1,6 +1,6 @@ # Tool Usage Notes -Tool signatures are provided automatically via function calling. This file +Tool signatures are provided automatically via function calling. This section documents the general tool contract and non-obvious usage patterns. ## General Tool Contract diff --git a/nanobot/utils/helpers.py b/nanobot/utils/helpers.py index 2a969298c..ae91bf394 100644 --- a/nanobot/utils/helpers.py +++ b/nanobot/utils/helpers.py @@ -576,7 +576,7 @@ def build_status_content( def sync_workspace_templates(workspace: Path, silent: bool = False) -> list[str]: - """Sync bundled templates to workspace. Only creates missing files.""" + """Sync bundled templates to workspace. Creates missing files without overwriting user files.""" from importlib.resources import files as pkg_files try: @@ -589,10 +589,11 @@ def sync_workspace_templates(workspace: Path, silent: bool = False) -> list[str] added: list[str] = [] def _write(src, dest: Path): + content = src.read_text(encoding="utf-8") if src else "" if dest.exists(): return dest.parent.mkdir(parents=True, exist_ok=True) - dest.write_text(src.read_text(encoding="utf-8") if src else "", encoding="utf-8") + dest.write_text(content, encoding="utf-8") added.append(str(dest.relative_to(workspace))) for item in tpl.iterdir(): diff --git a/tests/agent/test_context_builder.py b/tests/agent/test_context_builder.py index abd934b0a..a36c0a30a 100644 --- a/tests/agent/test_context_builder.py +++ b/tests/agent/test_context_builder.py @@ -139,6 +139,13 @@ class TestLoadBootstrapFiles: for name in ContextBuilder.BOOTSTRAP_FILES: assert f"## {name}" in result + def test_legacy_tools_md_is_not_bootstrapped(self, tmp_path): + (tmp_path / "TOOLS.md").write_text("workspace tool notes", encoding="utf-8") + builder = _builder(tmp_path) + result = builder._load_bootstrap_files() + assert "TOOLS.md" not in result + assert "workspace tool notes" not in result + def test_utf8_content(self, tmp_path): (tmp_path / "AGENTS.md").write_text("用中文回复", encoding="utf-8") builder = _builder(tmp_path) @@ -176,11 +183,11 @@ class TestIsTemplateContent: # --------------------------------------------------------------------------- -class TestBundledToolsTemplate: - def test_tools_template_balances_general_and_coding_workflows(self): +class TestBundledToolContract: + def test_tool_contract_balances_general_and_coding_workflows(self): from importlib.resources import files as pkg_files - tpl = pkg_files("nanobot") / "templates" / "TOOLS.md" + tpl = pkg_files("nanobot") / "templates" / "agent" / "tool_contract.md" content = tpl.read_text(encoding="utf-8") assert "## General Tool Contract" in content @@ -193,6 +200,14 @@ class TestBundledToolsTemplate: assert "## Scheduling and Background Work" in content assert "pure coding" not in content.lower() + def test_tool_contract_is_injected_without_workspace_file(self, tmp_path): + builder = _builder(tmp_path) + prompt = builder.build_system_prompt() + + assert "# Tool Usage Notes" in prompt + assert "## General Tool Contract" in prompt + assert "Do not use `exec` as a universal workaround" in prompt + # --------------------------------------------------------------------------- # _build_user_content diff --git a/tests/agent/test_onboard_logic.py b/tests/agent/test_onboard_logic.py index 11a284bb5..762da4f31 100644 --- a/tests/agent/test_onboard_logic.py +++ b/tests/agent/test_onboard_logic.py @@ -346,6 +346,26 @@ class TestSyncWorkspaceTemplates: content = (workspace / "AGENTS.md").read_text() assert content == "existing content" + def test_does_not_create_tools_md(self, tmp_path): + """Tool contract is injected internally, not copied into user workspaces.""" + workspace = tmp_path / "workspace" + + added = sync_workspace_templates(workspace, silent=True) + + assert "TOOLS.md" not in added + assert not (workspace / "TOOLS.md").exists() + + def test_preserves_existing_tools_md_without_overwriting(self, tmp_path): + """Legacy user workspaces may have TOOLS.md; sync should leave it untouched.""" + workspace = tmp_path / "workspace" + workspace.mkdir(parents=True) + tools_path = workspace / "TOOLS.md" + tools_path.write_text("custom tool notes", encoding="utf-8") + + sync_workspace_templates(workspace, silent=True) + + assert tools_path.read_text(encoding="utf-8") == "custom tool notes" + def test_creates_memory_directory(self, tmp_path): """Should create memory directory structure.""" workspace = tmp_path / "workspace"