fix(agent): preserve agent-owned state in project workspaces (#4945)

This commit is contained in:
chengyongru
2026-07-22 17:25:22 +08:00
committed by GitHub
parent 4cd6eb6c38
commit b189a37648
23 changed files with 560 additions and 89 deletions
+66
View File
@@ -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)