fix(agent): preserve agent-owned state in project workspaces (#4945)
This commit is contained in:
@@ -11,6 +11,7 @@ from nanobot.agent.tools.filesystem import FileToolsConfig
|
||||
from nanobot.bus.queue import MessageBus
|
||||
from nanobot.config.schema import ToolsConfig
|
||||
from nanobot.providers.base import GenerationSettings, LLMProvider
|
||||
from nanobot.security.workspace_access import build_workspace_scope
|
||||
from nanobot.utils.llm_runtime import LLMRuntime
|
||||
|
||||
|
||||
@@ -82,6 +83,71 @@ def test_subagent_respects_file_tool_toggle(tmp_path):
|
||||
assert file_tools.isdisjoint(tools.tool_names)
|
||||
|
||||
|
||||
def test_subagent_prompt_explains_grouped_skill_paths(tmp_path):
|
||||
agent_workspace = tmp_path / "agent"
|
||||
project = tmp_path / "project"
|
||||
global_skill = agent_workspace / "skills" / "global-custom" / "SKILL.md"
|
||||
project_skill = project / "skills" / "project-custom" / "SKILL.md"
|
||||
global_skill.parent.mkdir(parents=True)
|
||||
project_skill.parent.mkdir(parents=True)
|
||||
global_skill.write_text("---\ndescription: global skill\n---\nGlobal", encoding="utf-8")
|
||||
project_skill.write_text("---\ndescription: project skill\n---\nProject", encoding="utf-8")
|
||||
manager = SubagentManager(
|
||||
workspace=agent_workspace,
|
||||
bus=MessageBus(),
|
||||
max_tool_result_chars=16_000,
|
||||
)
|
||||
|
||||
prompt = manager._build_subagent_prompt(workspace=project)
|
||||
|
||||
assert "one absolute root and relative SKILL.md paths" in prompt
|
||||
assert "Join them when using `read_file`" in prompt
|
||||
assert f"Current project workspace: {project.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 "global-custom" in prompt
|
||||
assert "project-custom" not in prompt
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subagent_keeps_project_runtime_scope_with_agent_owned_tools(tmp_path):
|
||||
agent_workspace = tmp_path / "agent"
|
||||
project = tmp_path / "project"
|
||||
agent_workspace.mkdir()
|
||||
project.mkdir()
|
||||
provider = MagicMock(spec=LLMProvider)
|
||||
provider.get_default_model.return_value = "test"
|
||||
manager = SubagentManager(
|
||||
workspace=agent_workspace,
|
||||
bus=MessageBus(),
|
||||
max_tool_result_chars=16_000,
|
||||
)
|
||||
manager.runner.run = AsyncMock(
|
||||
return_value=AgentRunResult(final_content="ok", messages=[], stop_reason="completed")
|
||||
)
|
||||
manager._announce_result = AsyncMock()
|
||||
status = SubagentStatus(
|
||||
task_id="t1",
|
||||
label="label",
|
||||
task_description="task",
|
||||
started_at=0.0,
|
||||
)
|
||||
|
||||
await manager._run_subagent(
|
||||
"t1",
|
||||
"task",
|
||||
"label",
|
||||
{"channel": "websocket", "chat_id": "direct"},
|
||||
status,
|
||||
_runtime(provider),
|
||||
workspace_scope=build_workspace_scope(project, "restricted"),
|
||||
)
|
||||
|
||||
spec = manager.runner.run.call_args.args[0]
|
||||
assert spec.workspace == project
|
||||
assert spec.tools.get("read_file")._workspace == agent_workspace.resolve()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subagent_forwards_fail_on_tool_error_to_runner(tmp_path):
|
||||
provider = MagicMock(spec=LLMProvider)
|
||||
|
||||
Reference in New Issue
Block a user