mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 08:13:11 +03:00
fix(agent): honor selected project workspace
This commit is contained in:
@@ -112,6 +112,14 @@ class ContextBuilder:
|
|||||||
|
|
||||||
parts.append(render_template("agent/tool_contract.md"))
|
parts.append(render_template("agent/tool_contract.md"))
|
||||||
|
|
||||||
|
project_path = root.expanduser().resolve()
|
||||||
|
if project_path != self.workspace.expanduser().resolve():
|
||||||
|
parts.append(
|
||||||
|
"# Current Project\n\n"
|
||||||
|
f"Working directory: {project_path}\n"
|
||||||
|
"Use it as the default root for project files and relative tool paths."
|
||||||
|
)
|
||||||
|
|
||||||
if include_memory:
|
if include_memory:
|
||||||
memory = self.memory.read_memory()
|
memory = self.memory.read_memory()
|
||||||
if memory and not self._is_template_content(memory, "memory/MEMORY.md"):
|
if memory and not self._is_template_content(memory, "memory/MEMORY.md"):
|
||||||
|
|||||||
@@ -49,6 +49,29 @@ def test_system_prompt_stays_stable_when_clock_changes(tmp_path, monkeypatch) ->
|
|||||||
assert prompt1 == prompt2
|
assert prompt1 == prompt2
|
||||||
|
|
||||||
|
|
||||||
|
def test_selected_project_path_follows_shared_cache_prefix(tmp_path) -> None:
|
||||||
|
"""Project paths must not invalidate the stable identity and tool contract prefix."""
|
||||||
|
agent_home = tmp_path / "agent-home"
|
||||||
|
project_a = tmp_path / "project-a"
|
||||||
|
project_b = tmp_path / "project-b"
|
||||||
|
agent_home.mkdir()
|
||||||
|
project_a.mkdir()
|
||||||
|
project_b.mkdir()
|
||||||
|
builder = ContextBuilder(agent_home)
|
||||||
|
|
||||||
|
prompt_a = builder.build_system_prompt(workspace=project_a)
|
||||||
|
prompt_b = builder.build_system_prompt(workspace=project_b)
|
||||||
|
marker = "# Current Project"
|
||||||
|
prefix_a = prompt_a[: prompt_a.index(marker)]
|
||||||
|
prefix_b = prompt_b[: prompt_b.index(marker)]
|
||||||
|
|
||||||
|
assert prefix_a == prefix_b
|
||||||
|
assert "# Tool Usage Notes" in prefix_a
|
||||||
|
assert str(project_a.resolve()) not in prefix_a
|
||||||
|
assert str(project_b.resolve()) not in prefix_b
|
||||||
|
assert prompt_a == builder.build_system_prompt(workspace=project_a)
|
||||||
|
|
||||||
|
|
||||||
def test_system_prompt_reflects_current_dream_memory_contract(tmp_path) -> None:
|
def test_system_prompt_reflects_current_dream_memory_contract(tmp_path) -> None:
|
||||||
workspace = _make_workspace(tmp_path)
|
workspace = _make_workspace(tmp_path)
|
||||||
builder = ContextBuilder(workspace)
|
builder = ContextBuilder(workspace)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from nanobot.agent.context import ContextBuilder
|
||||||
from nanobot.agent.tools.cli_apps import CliAppsTool
|
from nanobot.agent.tools.cli_apps import CliAppsTool
|
||||||
from nanobot.agent.tools.context import RequestContext, ToolContext, request_context
|
from nanobot.agent.tools.context import RequestContext, ToolContext, request_context
|
||||||
from nanobot.agent.tools.filesystem import ReadFileTool, WriteFileTool
|
from nanobot.agent.tools.filesystem import ReadFileTool, WriteFileTool
|
||||||
@@ -21,6 +22,7 @@ from nanobot.config.schema import ImageGenerationToolConfig, ProviderConfig, Too
|
|||||||
from nanobot.security.workspace_access import (
|
from nanobot.security.workspace_access import (
|
||||||
WORKSPACE_SCOPE_METADATA_KEY,
|
WORKSPACE_SCOPE_METADATA_KEY,
|
||||||
WorkspaceScopeError,
|
WorkspaceScopeError,
|
||||||
|
WorkspaceScopeResolver,
|
||||||
bind_workspace_scope,
|
bind_workspace_scope,
|
||||||
default_workspace_scope,
|
default_workspace_scope,
|
||||||
reset_workspace_scope,
|
reset_workspace_scope,
|
||||||
@@ -101,6 +103,34 @@ def test_workspace_scope_accepts_home_relative_project_path(
|
|||||||
assert scope.metadata()["project_path"] == str(project.resolve())
|
assert scope.metadata()["project_path"] == str(project.resolve())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("access_mode", ["restricted", "full"])
|
||||||
|
def test_selected_websocket_project_is_visible_to_the_model(
|
||||||
|
tmp_path: Path,
|
||||||
|
access_mode: str,
|
||||||
|
) -> None:
|
||||||
|
agent_home = tmp_path / "agent-home"
|
||||||
|
project = tmp_path / "project"
|
||||||
|
agent_home.mkdir()
|
||||||
|
project.mkdir()
|
||||||
|
resolver = WorkspaceScopeResolver(agent_home, default_restrict_to_workspace=False)
|
||||||
|
|
||||||
|
scope = resolver.for_turn(
|
||||||
|
channel="websocket",
|
||||||
|
message_metadata={
|
||||||
|
WORKSPACE_SCOPE_METADATA_KEY: {
|
||||||
|
"project_path": str(project),
|
||||||
|
"access_mode": access_mode,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
session_metadata=None,
|
||||||
|
)
|
||||||
|
prompt = ContextBuilder(agent_home).build_system_prompt(workspace=scope.project_path)
|
||||||
|
|
||||||
|
assert prompt.index("# Tool Usage Notes") < prompt.index("# Current Project")
|
||||||
|
assert f"Working directory: {project.resolve()}" in prompt
|
||||||
|
assert "Use it as the default root for project files" in prompt
|
||||||
|
|
||||||
|
|
||||||
def test_workspace_scope_metadata_falls_back_for_stale_session(tmp_path: Path) -> None:
|
def test_workspace_scope_metadata_falls_back_for_stale_session(tmp_path: Path) -> None:
|
||||||
scope = workspace_scope_from_metadata(
|
scope = workspace_scope_from_metadata(
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user