mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 16:21:50 +03:00
fix(agent): keep default prompt paths relative
This commit is contained in:
@@ -123,7 +123,10 @@ class ContextBuilder:
|
|||||||
if active_content:
|
if active_content:
|
||||||
parts.append(f"# Active Skills\n\n{active_content}")
|
parts.append(f"# Active Skills\n\n{active_content}")
|
||||||
|
|
||||||
skills_summary = self.skills.build_skills_summary(exclude=set(active_skills))
|
skills_summary = self.skills.build_skills_summary(
|
||||||
|
exclude=set(active_skills),
|
||||||
|
workspace=root,
|
||||||
|
)
|
||||||
if skills_summary:
|
if skills_summary:
|
||||||
parts.append(render_template("agent/skills_section.md", skills_summary=skills_summary))
|
parts.append(render_template("agent/skills_section.md", skills_summary=skills_summary))
|
||||||
|
|
||||||
|
|||||||
+16
-2
@@ -201,7 +201,12 @@ class SkillsLoader:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def build_skills_summary(self, exclude: set[str] | None = None) -> str:
|
def build_skills_summary(
|
||||||
|
self,
|
||||||
|
exclude: set[str] | None = None,
|
||||||
|
*,
|
||||||
|
workspace: Path | None = None,
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Build a summary of all skills (name, description, path, availability).
|
Build a summary of all skills (name, description, path, availability).
|
||||||
|
|
||||||
@@ -210,6 +215,7 @@ class SkillsLoader:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
exclude: Set of skill names to omit from the summary.
|
exclude: Set of skill names to omit from the summary.
|
||||||
|
workspace: Effective project workspace used to choose safe display paths.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Markdown-formatted skills summary.
|
Markdown-formatted skills summary.
|
||||||
@@ -218,6 +224,9 @@ class SkillsLoader:
|
|||||||
if not all_skills:
|
if not all_skills:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
agent_workspace = self.workspace.expanduser().resolve()
|
||||||
|
project_workspace = (workspace or self.workspace).expanduser().resolve()
|
||||||
|
use_relative_roots = project_workspace == agent_workspace
|
||||||
sections: list[str] = []
|
sections: list[str] = []
|
||||||
groups = (
|
groups = (
|
||||||
("Workspace skills", "workspace", self.workspace_skills),
|
("Workspace skills", "workspace", self.workspace_skills),
|
||||||
@@ -233,7 +242,12 @@ class SkillsLoader:
|
|||||||
if not entries:
|
if not entries:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
lines = [f"### {label} (`{root.expanduser().resolve()}`)"]
|
resolved_root = root.expanduser().resolve()
|
||||||
|
if use_relative_roots:
|
||||||
|
display_root = Path("plugins" if source == "plugin" else "skills")
|
||||||
|
else:
|
||||||
|
display_root = resolved_root
|
||||||
|
lines = [f"### {label} (`{display_root}`)"]
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
skill_name = entry["name"]
|
skill_name = entry["name"]
|
||||||
meta = self._get_skill_meta(skill_name)
|
meta = self._get_skill_meta(skill_name)
|
||||||
|
|||||||
@@ -540,12 +540,17 @@ class SubagentManager:
|
|||||||
skills_summary = SkillsLoader(
|
skills_summary = SkillsLoader(
|
||||||
self.workspace,
|
self.workspace,
|
||||||
disabled_skills=self.disabled_skills,
|
disabled_skills=self.disabled_skills,
|
||||||
).build_skills_summary()
|
).build_skills_summary(workspace=project_workspace)
|
||||||
|
history_log = (
|
||||||
|
str(agent_workspace / "memory" / "history.jsonl")
|
||||||
|
if agent_workspace != project_workspace
|
||||||
|
else "memory/history.jsonl"
|
||||||
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"agent/subagent_system.md",
|
"agent/subagent_system.md",
|
||||||
workspace=str(project_workspace),
|
workspace=str(project_workspace),
|
||||||
agent_workspace=str(agent_workspace),
|
agent_workspace=str(agent_workspace),
|
||||||
history_log=str(agent_workspace / "memory" / "history.jsonl"),
|
history_log=history_log,
|
||||||
skills_summary=skills_summary or "",
|
skills_summary=skills_summary or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ description: Search conversation history and understand Dream-managed profile an
|
|||||||
|
|
||||||
## Search Past Events
|
## Search Past Events
|
||||||
|
|
||||||
Use the absolute `History log` path shown in the system prompt. Always pass it to
|
Use the `History log` path shown in the system prompt. Always pass it to `grep`;
|
||||||
`grep`; never substitute a project-relative `memory/history.jsonl`, which may belong
|
never substitute a different project-relative `memory/history.jsonl`, which may belong
|
||||||
to the selected project. Each JSONL line contains `cursor`, `timestamp`, and `content`.
|
to the selected project. Each JSONL line contains `cursor`, `timestamp`, and `content`.
|
||||||
|
|
||||||
- For broad searches, start with `output_mode="count"` or the default
|
- For broad searches, start with `output_mode="count"` or the default
|
||||||
@@ -25,7 +25,7 @@ to the selected project. Each JSONL line contains `cursor`, `timestamp`, and `co
|
|||||||
- Use `fixed_strings=true` for literal timestamps or JSON fragments
|
- Use `fixed_strings=true` for literal timestamps or JSON fragments
|
||||||
- Use `head_limit` / `offset` to page through long histories
|
- Use `head_limit` / `offset` to page through long histories
|
||||||
|
|
||||||
Examples (replace `<history-log-path>` with the absolute path from the system prompt):
|
Examples (replace `<history-log-path>` with the path from the system prompt):
|
||||||
- `grep(pattern="keyword", path="<history-log-path>", case_insensitive=true)`
|
- `grep(pattern="keyword", path="<history-log-path>", case_insensitive=true)`
|
||||||
- `grep(pattern="2026-04-02 10:00", path="<history-log-path>", fixed_strings=true)`
|
- `grep(pattern="2026-04-02 10:00", path="<history-log-path>", fixed_strings=true)`
|
||||||
- `grep(pattern="keyword", path="<history-log-path>", output_mode="count", case_insensitive=true)`
|
- `grep(pattern="keyword", path="<history-log-path>", output_mode="count", case_insensitive=true)`
|
||||||
|
|||||||
@@ -2,14 +2,18 @@
|
|||||||
{{ runtime }}
|
{{ runtime }}
|
||||||
|
|
||||||
## Workspace
|
## Workspace
|
||||||
Your current project workspace is at: {{ workspace_path }}
|
|
||||||
{% if agent_workspace_path != workspace_path %}
|
{% if agent_workspace_path != workspace_path %}
|
||||||
Nanobot's agent workspace is at: {{ agent_workspace_path }}
|
Nanobot's agent workspace is at: {{ agent_workspace_path }}
|
||||||
{% endif %}
|
|
||||||
- Agent profile: {{ agent_workspace_path }}/SOUL.md and {{ agent_workspace_path }}/USER.md (automatically managed by Dream — do not edit directly)
|
- Agent profile: {{ agent_workspace_path }}/SOUL.md and {{ agent_workspace_path }}/USER.md (automatically managed by Dream — do not edit directly)
|
||||||
- Long-term memory: {{ agent_workspace_path }}/memory/MEMORY.md (automatically managed by Dream — do not edit directly)
|
- Long-term memory: {{ agent_workspace_path }}/memory/MEMORY.md (automatically managed by Dream — do not edit directly)
|
||||||
- History log: {{ agent_workspace_path }}/memory/history.jsonl (append-only JSONL; prefer built-in `grep` for search).
|
- History log: {{ agent_workspace_path }}/memory/history.jsonl (append-only JSONL; prefer built-in `grep` for search).
|
||||||
- Custom skills: {{ agent_workspace_path }}/skills/{% raw %}{skill-name}{% endraw %}/SKILL.md
|
- Custom skills: {{ agent_workspace_path }}/skills/{% raw %}{skill-name}{% endraw %}/SKILL.md
|
||||||
|
{% else %}
|
||||||
|
- Agent profile: SOUL.md and USER.md (automatically managed by Dream — do not edit directly)
|
||||||
|
- Long-term memory: memory/MEMORY.md (automatically managed by Dream — do not edit directly)
|
||||||
|
- History log: memory/history.jsonl (append-only JSONL; prefer built-in `grep` for search).
|
||||||
|
- Custom skills: skills/{% raw %}{skill-name}{% endraw %}/SKILL.md
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{{ platform_policy }}
|
{{ platform_policy }}
|
||||||
{% if channel == 'telegram' or channel == 'qq' or channel == 'discord' %}
|
{% if channel == 'telegram' or channel == 'qq' or channel == 'discord' %}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Skills
|
# Skills
|
||||||
|
|
||||||
The following skills extend your capabilities. Each group lists one absolute root and relative SKILL.md paths; join them when using `read_file`.
|
The following skills extend your capabilities. Each group lists one root and relative SKILL.md paths; join them when using `read_file`.
|
||||||
|
|
||||||
{{ skills_summary }}
|
{{ skills_summary }}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ Stay focused on the assigned task. Your final response will be reported back to
|
|||||||
{% include 'agent/_snippets/untrusted_content.md' %}
|
{% include 'agent/_snippets/untrusted_content.md' %}
|
||||||
|
|
||||||
## Workspace
|
## Workspace
|
||||||
Current project workspace: {{ workspace }}
|
|
||||||
{% if agent_workspace != workspace %}
|
{% if agent_workspace != workspace %}
|
||||||
Nanobot's agent workspace: {{ agent_workspace }}
|
Nanobot's agent workspace: {{ agent_workspace }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -15,7 +14,7 @@ History log: {{ history_log }}
|
|||||||
|
|
||||||
## Skills
|
## Skills
|
||||||
|
|
||||||
Each group lists one absolute root and relative SKILL.md paths. Join them when using `read_file`.
|
Each group lists one root and relative SKILL.md paths. Join them when using `read_file`.
|
||||||
|
|
||||||
{{ skills_summary }}
|
{{ skills_summary }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -84,7 +84,10 @@ def test_plugin_skill_lifecycle_and_precedence(tmp_path: Path) -> None:
|
|||||||
assert loader.get_explicitly_invoked_skills("Use $shared") == ["shared"]
|
assert loader.get_explicitly_invoked_skills("Use $shared") == ["shared"]
|
||||||
assert loader.get_always_skills() == ["shared"]
|
assert loader.get_always_skills() == ["shared"]
|
||||||
assert "Plugin body" in (loader.load_skill("shared") or "")
|
assert "Plugin body" in (loader.load_skill("shared") or "")
|
||||||
assert "`demo/skills/shared/SKILL.md`" in loader.build_skills_summary()
|
summary = loader.build_skills_summary()
|
||||||
|
assert "### Agent Plugin skills (`plugins`)" in summary
|
||||||
|
assert "`demo/skills/shared/SKILL.md`" in summary
|
||||||
|
assert str(tmp_path.resolve()) not in summary
|
||||||
|
|
||||||
set_agent_plugin_enabled(tmp_path, "demo", False)
|
set_agent_plugin_enabled(tmp_path, "demo", False)
|
||||||
assert [entry["source"] for entry in loader.list_skills()] == ["builtin"]
|
assert [entry["source"] for entry in loader.list_skills()] == ["builtin"]
|
||||||
|
|||||||
@@ -309,6 +309,14 @@ class TestBuildSystemPrompt:
|
|||||||
result = builder.build_system_prompt()
|
result = builder.build_system_prompt()
|
||||||
assert "workspace" in result.lower() or "python" in result.lower()
|
assert "workspace" in result.lower() or "python" in result.lower()
|
||||||
|
|
||||||
|
def test_default_identity_uses_relative_agent_paths(self, tmp_path):
|
||||||
|
result = ContextBuilder(tmp_path)._get_identity()
|
||||||
|
|
||||||
|
assert str(tmp_path.resolve()) not in result
|
||||||
|
assert "Agent profile: SOUL.md and USER.md" in result
|
||||||
|
assert "History log: memory/history.jsonl" in result
|
||||||
|
assert "Custom skills: skills/{skill-name}/SKILL.md" in result
|
||||||
|
|
||||||
def test_selected_project_identity_keeps_agent_data_in_agent_workspace(self, tmp_path):
|
def test_selected_project_identity_keeps_agent_data_in_agent_workspace(self, tmp_path):
|
||||||
agent_home = tmp_path / "agent-home"
|
agent_home = tmp_path / "agent-home"
|
||||||
project = tmp_path / "project"
|
project = tmp_path / "project"
|
||||||
@@ -317,7 +325,7 @@ class TestBuildSystemPrompt:
|
|||||||
|
|
||||||
result = ContextBuilder(agent_home)._get_identity(workspace=project)
|
result = ContextBuilder(agent_home)._get_identity(workspace=project)
|
||||||
|
|
||||||
assert f"current project workspace is at: {project.resolve()}" in result
|
assert str(project.resolve()) not in result
|
||||||
assert f"agent workspace is at: {agent_home.resolve()}" in result
|
assert f"agent workspace is at: {agent_home.resolve()}" in result
|
||||||
assert f"{agent_home.resolve()}/SOUL.md" in result
|
assert f"{agent_home.resolve()}/SOUL.md" in result
|
||||||
assert f"{project.resolve()}/SOUL.md" not in result
|
assert f"{project.resolve()}/SOUL.md" not in result
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ def test_disabled_skills_excluded_from_build_skills_summary(tmp_path: Path) -> N
|
|||||||
assert "beta" in summary
|
assert "beta" in summary
|
||||||
|
|
||||||
|
|
||||||
def test_build_skills_summary_groups_paths_by_root(tmp_path: Path) -> None:
|
def test_build_skills_summary_uses_relative_roots_in_agent_workspace(tmp_path: Path) -> None:
|
||||||
workspace = tmp_path / "ws"
|
workspace = tmp_path / "ws"
|
||||||
workspace_skills = workspace / "skills"
|
workspace_skills = workspace / "skills"
|
||||||
workspace_skills.mkdir(parents=True)
|
workspace_skills.mkdir(parents=True)
|
||||||
@@ -308,14 +308,34 @@ def test_build_skills_summary_groups_paths_by_root(tmp_path: Path) -> None:
|
|||||||
|
|
||||||
summary = SkillsLoader(workspace, builtin_skills_dir=builtin).build_skills_summary()
|
summary = SkillsLoader(workspace, builtin_skills_dir=builtin).build_skills_summary()
|
||||||
|
|
||||||
assert summary.count(str(workspace_skills)) == 1
|
assert str(workspace_skills) not in summary
|
||||||
assert summary.count(str(builtin)) == 1
|
assert str(builtin) not in summary
|
||||||
assert str(workspace_path) not in summary
|
assert str(workspace_path) not in summary
|
||||||
assert str(builtin_path) not in summary
|
assert str(builtin_path) not in summary
|
||||||
|
assert summary.count("(`skills`)") == 2
|
||||||
assert "`alpha/SKILL.md`" in summary
|
assert "`alpha/SKILL.md`" in summary
|
||||||
assert "`beta/SKILL.md`" in summary
|
assert "`beta/SKILL.md`" in summary
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_skills_summary_keeps_absolute_roots_for_selected_project(tmp_path: Path) -> None:
|
||||||
|
workspace = tmp_path / "ws"
|
||||||
|
workspace_skills = workspace / "skills"
|
||||||
|
workspace_skills.mkdir(parents=True)
|
||||||
|
_write_skill(workspace_skills, "alpha", body="# Alpha")
|
||||||
|
builtin = tmp_path / "builtin"
|
||||||
|
_write_skill(builtin, "beta", body="# Beta")
|
||||||
|
project = tmp_path / "project"
|
||||||
|
project.mkdir()
|
||||||
|
|
||||||
|
summary = SkillsLoader(workspace, builtin_skills_dir=builtin).build_skills_summary(
|
||||||
|
workspace=project,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert summary.count(str(workspace_skills.resolve())) == 1
|
||||||
|
assert summary.count(str(builtin.resolve())) == 1
|
||||||
|
assert str(project.resolve()) not in summary
|
||||||
|
|
||||||
|
|
||||||
def test_bundled_update_setup_description_is_valid_yaml(tmp_path: Path) -> None:
|
def test_bundled_update_setup_description_is_valid_yaml(tmp_path: Path) -> None:
|
||||||
metadata = SkillsLoader(tmp_path).get_skill_metadata("update-setup")
|
metadata = SkillsLoader(tmp_path).get_skill_metadata("update-setup")
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ def test_subagent_respects_file_tool_toggle(tmp_path):
|
|||||||
assert file_tools.isdisjoint(tools.tool_names)
|
assert file_tools.isdisjoint(tools.tool_names)
|
||||||
|
|
||||||
|
|
||||||
def test_subagent_prompt_explains_grouped_skill_paths(tmp_path):
|
def test_subagent_prompt_keeps_agent_paths_for_selected_project(tmp_path):
|
||||||
agent_workspace = tmp_path / "agent"
|
agent_workspace = tmp_path / "agent"
|
||||||
project = tmp_path / "project"
|
project = tmp_path / "project"
|
||||||
global_skill = agent_workspace / "skills" / "global-custom" / "SKILL.md"
|
global_skill = agent_workspace / "skills" / "global-custom" / "SKILL.md"
|
||||||
@@ -100,15 +100,32 @@ def test_subagent_prompt_explains_grouped_skill_paths(tmp_path):
|
|||||||
|
|
||||||
prompt = manager._build_subagent_prompt(workspace=project)
|
prompt = manager._build_subagent_prompt(workspace=project)
|
||||||
|
|
||||||
assert "one absolute root and relative SKILL.md paths" in prompt
|
assert "one root and relative SKILL.md paths" in prompt
|
||||||
assert "Join them when using `read_file`" in prompt
|
assert "Join them when using `read_file`" in prompt
|
||||||
assert f"Current project workspace: {project.resolve()}" in prompt
|
assert str(project.resolve()) not in prompt
|
||||||
assert f"Nanobot's agent workspace: {agent_workspace.resolve()}" in prompt
|
assert f"Nanobot's agent workspace: {agent_workspace.resolve()}" in prompt
|
||||||
assert f"History log: {agent_workspace.resolve() / 'memory' / 'history.jsonl'}" in prompt
|
assert f"History log: {agent_workspace.resolve() / 'memory' / 'history.jsonl'}" in prompt
|
||||||
assert "global-custom" in prompt
|
assert "global-custom" in prompt
|
||||||
assert "project-custom" not in prompt
|
assert "project-custom" not in prompt
|
||||||
|
|
||||||
|
|
||||||
|
def test_subagent_prompt_uses_relative_paths_in_agent_workspace(tmp_path):
|
||||||
|
skill = tmp_path / "skills" / "custom" / "SKILL.md"
|
||||||
|
skill.parent.mkdir(parents=True)
|
||||||
|
skill.write_text("---\ndescription: custom skill\n---\nCustom", encoding="utf-8")
|
||||||
|
manager = SubagentManager(
|
||||||
|
workspace=tmp_path,
|
||||||
|
bus=MessageBus(),
|
||||||
|
max_tool_result_chars=16_000,
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = manager._build_subagent_prompt()
|
||||||
|
|
||||||
|
assert str(tmp_path.resolve()) not in prompt
|
||||||
|
assert "History log: memory/history.jsonl" in prompt
|
||||||
|
assert "### Workspace skills (`skills`)" in prompt
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_subagent_keeps_project_runtime_scope_with_agent_owned_tools(tmp_path):
|
async def test_subagent_keeps_project_runtime_scope_with_agent_owned_tools(tmp_path):
|
||||||
agent_workspace = tmp_path / "agent"
|
agent_workspace = tmp_path / "agent"
|
||||||
|
|||||||
Reference in New Issue
Block a user