Files
roboco/tests/unit/agents/test_tool_load_directive_layer.py
T
Renn F 197b1576c3 fix(prompts): hoist ToolSearch activation to top of system prompt
Smoke-7: be-dev-1 hit "Edit exists but is not enabled in this context."
Claude Code v2.1.69+ defers built-in tools (Edit, Write, Read, etc.)
behind a ToolSearch call. Weak models (minimax-m2.7) skip soft
directives buried in the briefing.

Also: 4 role prompts (developer, cell_pm, main_pm, board) claimed
"no ToolSearch needed" — a lie that compounds the problem. The
manifest registers MCP tools; built-in tools are still deferred.

Fix: compose_prompt now prepends a tool-load directive layer as the
FIRST block in the system prompt. It names the exact ToolSearch call
the role needs:
- developer/documenter: Read, Bash, Grep, Glob, Task, TodoWrite, Edit, Write
- qa/pm/board: Read, Bash, Grep, Glob, Task, TodoWrite (no Edit/Write)

The directive includes the failure mode it prevents so the model
understands what skipping the call causes.

Updated role-prompt lines that lied about ToolSearch.

7 new tests pin: directive is the first block; developer/documenter
get Edit/Write; qa/pm don't; failure-mode message is present.
2026-05-15 03:14:43 +02:00

96 lines
4.1 KiB
Python

"""Smoke-7: system prompt opens with a ToolSearch directive that activates
deferred built-in tools (Edit, Write, Read, ...).
Original bug: be-dev-1 called Edit and got "Edit exists but is not enabled
in this context" because Claude Code v2.1.69+ defers built-in tools behind
a ToolSearch call. The role prompts said "no ToolSearch needed" — a lie
for built-in tools — so weak models skipped the activation step.
Fix: compose_prompt now prepends a tool-load directive layer that names
the exact ToolSearch call for the role. It's the highest-priority block in
the system prompt so even weak models follow it.
"""
from __future__ import annotations
from roboco.agents.factories._base import compose_prompt
from roboco.models import AgentRole, Team
def _composed_prompt_for(role: AgentRole, team: Team | None = None) -> str:
"""Compose the prompt for a role and team."""
return compose_prompt(role, team, agent_slug="test-agent")
def test_developer_prompt_starts_with_tool_load_directive() -> None:
"""Developer system prompt begins with the ToolSearch activation block."""
prompt = _composed_prompt_for(AgentRole.DEVELOPER, Team.BACKEND)
assert prompt.startswith("# FIRST ACTION REQUIRED"), (
f"Developer prompt must lead with the tool-load directive. "
f"Got first 80 chars: {prompt[:80]!r}"
)
def test_developer_directive_names_edit_and_write() -> None:
"""Developer ToolSearch call lists Edit + Write (the smoke-7 wedge)."""
prompt = _composed_prompt_for(AgentRole.DEVELOPER, Team.BACKEND)
assert 'ToolSearch(query="select:' in prompt
# Find the ToolSearch line
line = next(line for line in prompt.splitlines() if "ToolSearch(query=" in line)
assert "Edit" in line, f"Developer ToolSearch missing Edit: {line!r}"
assert "Write" in line
def test_documenter_directive_names_edit_and_write() -> None:
"""Documenter needs Edit/Write too — they author docs."""
prompt = _composed_prompt_for(AgentRole.DOCUMENTER, Team.BACKEND)
line = next(line for line in prompt.splitlines() if "ToolSearch(query=" in line)
assert "Edit" in line
assert "Write" in line
def _tool_list_from_directive(prompt: str) -> list[str]:
"""Extract the comma-separated tool list from the ToolSearch call."""
line = next(line for line in prompt.splitlines() if "ToolSearch(query=" in line)
# `ToolSearch(query="select:Read,Bash,...")` — pull out the names.
start = line.find("select:") + len("select:")
end = line.find('"', start)
return line[start:end].split(",")
def test_qa_directive_excludes_edit_and_write() -> None:
"""QA reads but doesn't author — directive must NOT activate Edit/Write."""
tools = _tool_list_from_directive(_composed_prompt_for(AgentRole.QA, Team.BACKEND))
assert "Edit" not in tools, f"QA must not activate Edit: tools={tools}"
assert "Write" not in tools
assert "Read" in tools
assert "Bash" in tools
def test_pm_directives_exclude_edit_and_write() -> None:
"""PMs coordinate; they don't author code. No Edit/Write activation."""
for role in (AgentRole.MAIN_PM, AgentRole.CELL_PM):
tools = _tool_list_from_directive(_composed_prompt_for(role))
assert "Edit" not in tools, f"{role.value} must not activate Edit: {tools}"
assert "Write" not in tools
def test_directive_explains_why_it_matters() -> None:
"""The block must mention 'Edit exists but is not enabled' so the agent
understands what skipping the call causes."""
prompt = _composed_prompt_for(AgentRole.DEVELOPER, Team.BACKEND)
assert "Edit exists but is not enabled" in prompt
def test_directive_is_first_layer_before_lifecycle() -> None:
"""First Action block precedes the lifecycle and base layers."""
prompt = _composed_prompt_for(AgentRole.DEVELOPER, Team.BACKEND)
first_idx = prompt.find("# FIRST ACTION REQUIRED")
lifecycle_idx = prompt.find("Lifecycle")
base_idx = prompt.find("RoboCo Agent — Base")
assert first_idx == 0
if lifecycle_idx > -1:
assert first_idx < lifecycle_idx
if base_idx > -1:
assert first_idx < base_idx