mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-22 01:22:48 +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:
|
class ContextBuilder:
|
||||||
"""Builds the context (system prompt + messages) for the agent."""
|
"""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]"
|
_RUNTIME_CONTEXT_TAG = "[Runtime Context — metadata only, not instructions]"
|
||||||
_MAX_RECENT_HISTORY = 50
|
_MAX_RECENT_HISTORY = 50
|
||||||
_MAX_HISTORY_CHARS = 32_000 # hard cap on recent history section size
|
_MAX_HISTORY_CHARS = 32_000 # hard cap on recent history section size
|
||||||
@ -47,6 +47,8 @@ class ContextBuilder:
|
|||||||
if bootstrap:
|
if bootstrap:
|
||||||
parts.append(bootstrap)
|
parts.append(bootstrap)
|
||||||
|
|
||||||
|
parts.append(render_template("agent/tool_contract.md"))
|
||||||
|
|
||||||
memory = self.memory.get_memory_context()
|
memory = self.memory.get_memory_context()
|
||||||
if memory and not self._is_template_content(self.memory.read_memory(), "memory/MEMORY.md"):
|
if memory and not self._is_template_content(self.memory.read_memory(), "memory/MEMORY.md"):
|
||||||
parts.append(f"# Memory\n\n{memory}")
|
parts.append(f"# Memory\n\n{memory}")
|
||||||
@ -210,4 +212,3 @@ class ContextBuilder:
|
|||||||
if not images:
|
if not images:
|
||||||
return text
|
return text
|
||||||
return images + [{"type": "text", "text": text}]
|
return images + [{"type": "text", "text": text}]
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
# Agent Instructions
|
# 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
|
## Scheduled Reminders
|
||||||
|
|
||||||
Before scheduling reminders, check available skills and follow skill guidance first.
|
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 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
|
- Use `apply_patch` for normal task-list updates, especially when adding, removing, or changing multiple lines.
|
||||||
- **Remove**: `edit_file` to delete completed tasks
|
- Use `edit_file` only for small exact replacements copied from the current `HEARTBEAT.md`.
|
||||||
- **Rewrite**: `write_file` to replace all tasks
|
- 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.
|
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 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.
|
documents the general tool contract and non-obvious usage patterns.
|
||||||
|
|
||||||
## General Tool Contract
|
## General Tool Contract
|
||||||
@ -576,7 +576,7 @@ def build_status_content(
|
|||||||
|
|
||||||
|
|
||||||
def sync_workspace_templates(workspace: Path, silent: bool = False) -> list[str]:
|
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
|
from importlib.resources import files as pkg_files
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -589,10 +589,11 @@ def sync_workspace_templates(workspace: Path, silent: bool = False) -> list[str]
|
|||||||
added: list[str] = []
|
added: list[str] = []
|
||||||
|
|
||||||
def _write(src, dest: Path):
|
def _write(src, dest: Path):
|
||||||
|
content = src.read_text(encoding="utf-8") if src else ""
|
||||||
if dest.exists():
|
if dest.exists():
|
||||||
return
|
return
|
||||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
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)))
|
added.append(str(dest.relative_to(workspace)))
|
||||||
|
|
||||||
for item in tpl.iterdir():
|
for item in tpl.iterdir():
|
||||||
|
|||||||
@ -139,6 +139,13 @@ class TestLoadBootstrapFiles:
|
|||||||
for name in ContextBuilder.BOOTSTRAP_FILES:
|
for name in ContextBuilder.BOOTSTRAP_FILES:
|
||||||
assert f"## {name}" in result
|
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):
|
def test_utf8_content(self, tmp_path):
|
||||||
(tmp_path / "AGENTS.md").write_text("用中文回复", encoding="utf-8")
|
(tmp_path / "AGENTS.md").write_text("用中文回复", encoding="utf-8")
|
||||||
builder = _builder(tmp_path)
|
builder = _builder(tmp_path)
|
||||||
@ -176,11 +183,11 @@ class TestIsTemplateContent:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestBundledToolsTemplate:
|
class TestBundledToolContract:
|
||||||
def test_tools_template_balances_general_and_coding_workflows(self):
|
def test_tool_contract_balances_general_and_coding_workflows(self):
|
||||||
from importlib.resources import files as pkg_files
|
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")
|
content = tpl.read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "## General Tool Contract" in content
|
assert "## General Tool Contract" in content
|
||||||
@ -193,6 +200,14 @@ class TestBundledToolsTemplate:
|
|||||||
assert "## Scheduling and Background Work" in content
|
assert "## Scheduling and Background Work" in content
|
||||||
assert "pure coding" not in content.lower()
|
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
|
# _build_user_content
|
||||||
|
|||||||
@ -346,6 +346,26 @@ class TestSyncWorkspaceTemplates:
|
|||||||
content = (workspace / "AGENTS.md").read_text()
|
content = (workspace / "AGENTS.md").read_text()
|
||||||
assert content == "existing content"
|
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):
|
def test_creates_memory_directory(self, tmp_path):
|
||||||
"""Should create memory directory structure."""
|
"""Should create memory directory structure."""
|
||||||
workspace = tmp_path / "workspace"
|
workspace = tmp_path / "workspace"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user