mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-21 17:12:32 +00:00
refactor(agent): internalize tool contract prompt
This commit is contained in:
parent
581faa34f7
commit
d29fcaf5d1
@ -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}]
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
@ -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():
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user