Files
roboco/tests/unit/agents/test_tool_load_directive_layer.py
T
718d7dd83e Fix: open findings cleanup (#122)
* refactor(usage): remove the unconsumed per-agent USAGE_UPDATE event

USAGE_UPDATE was published per active agent each sweep, bridged, and
broadcast to /ws/system, but no panel client ever consumed it — the
dashboard reads only the aggregate USAGE_SNAPSHOT. Every emission was
wasted event-bus and WebSocket traffic.

Drop the UsageUpdate payload, publish_usage_update and its throttle, the
EventType member, and the bridge subscription. Keep USAGE_SNAPSHOT, which
already carries the per-agent breakdown, so no live data is lost.

* refactor(prompter): remove the legacy local-LLM HTTP endpoints

The panel uses only the live SDK-intake path (/prompter/live/*); the legacy
/prompter/chat, /draft and /sessions/* endpoints — backed by the local Ollama
LLM with hardcoded prompts — had no remaining caller. Remove the router, its
mount in app.py, and its integration test. The live router and the shared
draft-confirmation service are untouched.

* refactor(prompter): drop the dead legacy local-LLM service + schemas

With the legacy HTTP endpoints gone, the local-LLM chat/draft/session methods,
their prompt constants, the ConfirmOverrides/TurnResult dataclasses, and the
entire prompter schema module had no production caller (only their own tests).
Remove them, keeping the live-intake path: create_task_from_draft /
confirm_live_draft, the enum/priority/team coercion, and the pure
description/readiness helpers.

* refactor(agents): stop granting the Task sub-agent tool to roles

Every agent role was granted the built-in Task tool, but no role prompt or
workflow uses it and there are no custom sub-agent definitions — so a Task call
only spawns a context-blind generic sub-agent that burns budget (ToolSearch,
the comment's stated use, is MCP-only and not callable in agent containers).

Drop Task from all three grant points in lockstep: the --tools spawn flag and
both _ROLE_BUILTIN_TOOLS maps (system-prompt + briefing layers), with a
regression guard added to each layer's test.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-06-12 23:10:21 +02:00

127 lines
5.0 KiB
Python

"""#167: the system prompt must NOT tell agents to ToolSearch built-ins.
Earlier (smoke-7) the prompt opened with a "# FIRST ACTION REQUIRED:
run ToolSearch to activate Edit/Write" block. That premise was false:
ToolSearch is MCP-only and never gates built-in tools, and it is not
even a callable tool in the agent runtime. Weak models chased the
nonexistent tool, concluded Edit/Write were unavailable, and rewrote
whole files via destructive shell redirection. The real cause of
"Edit exists but is not enabled in this context" was a permission bug
(global Write(*)/Edit(*) deny + single-slash path), fixed separately.
The directive layer now affirms the tools are loaded and ready, tells
agents NOT to call ToolSearch, and (for authoring roles) steers away
from whole-file shell redirection.
"""
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:
return compose_prompt(role, team, agent_slug="test-agent")
def test_prompt_no_longer_instructs_a_toolsearch_call() -> None:
"""No role prompt may instruct an actual ToolSearch(query=...) call."""
for role in (
AgentRole.DEVELOPER,
AgentRole.DOCUMENTER,
AgentRole.QA,
AgentRole.MAIN_PM,
AgentRole.CELL_PM,
):
prompt = _composed_prompt_for(role, Team.BACKEND)
assert "ToolSearch(query=" not in prompt, (
f"{role.value} prompt still instructs a ToolSearch call"
)
assert "are deferred" not in prompt, (
f"{role.value} prompt still claims built-ins are deferred"
)
def test_developer_prompt_starts_with_tools_ready_block() -> None:
"""Developer system prompt leads with the tools-ready affirmation."""
prompt = _composed_prompt_for(AgentRole.DEVELOPER, Team.BACKEND)
assert prompt.startswith("# Your tools are ready"), (
f"Developer prompt must lead with the tools-ready block. "
f"Got first 80 chars: {prompt[:80]!r}"
)
def _tool_names(prompt: str) -> list[str]:
"""Exact tool tokens from the 'available now: <names>.' enumeration.
Exact tokens matter: a substring check would treat 'TodoWrite' as
containing 'Write'.
"""
line = next(ln for ln in prompt.splitlines() if "available now:" in ln)
seg = line.split("available now: ", 1)[1]
# _base layer ends the list with '.'; orchestrator continues with
# '. Use them...'. Either way the names stop at the first period.
return seg.split(".", 1)[0].split(", ")
def test_developer_block_lists_edit_and_write_as_available() -> None:
"""Authoring roles are told Edit + Write are loaded and available."""
for role in (AgentRole.DEVELOPER, AgentRole.DOCUMENTER):
names = _tool_names(_composed_prompt_for(role, Team.BACKEND))
assert "Edit" in names and "Write" in names, (
f"{role.value} tools-ready line must list Edit + Write: {names!r}"
)
def test_developer_block_steers_away_from_shell_redirection() -> None:
"""The exact failure mode (clobber a file via bash) is called out."""
prompt = _composed_prompt_for(AgentRole.DEVELOPER, Team.BACKEND)
assert "shell redirection" in prompt
assert "Edit/Write" in prompt
def test_qa_block_excludes_edit_and_write() -> None:
"""QA reads/reviews — Edit/Write must not be listed as available."""
names = _tool_names(_composed_prompt_for(AgentRole.QA, Team.BACKEND))
assert "Edit" not in names and "Write" not in names, names
assert "Read" in names and "Bash" in names
def test_pm_blocks_exclude_edit_and_write() -> None:
for role in (AgentRole.MAIN_PM, AgentRole.CELL_PM):
names = _tool_names(_composed_prompt_for(role))
assert "Edit" not in names and "Write" not in names, (
f"{role.value} must not list Edit/Write: {names}"
)
def test_no_role_is_granted_the_task_subagent_tool() -> None:
"""Task (sub-agent dispatch) is dropped from the built-in tool grant.
No role prompt or workflow uses Task and there are no custom sub-agent
definitions, so a Task call only spawns a context-blind generic sub-agent
that burns budget. The tools-ready line must not advertise it for any role.
"""
for role in (
AgentRole.DEVELOPER,
AgentRole.DOCUMENTER,
AgentRole.QA,
AgentRole.MAIN_PM,
AgentRole.CELL_PM,
):
names = _tool_names(_composed_prompt_for(role, Team.BACKEND))
assert "Task" not in names, f"{role.value} must not list Task: {names}"
def test_block_is_first_layer_before_lifecycle() -> None:
"""Tools-ready block precedes the lifecycle and base layers."""
prompt = _composed_prompt_for(AgentRole.DEVELOPER, Team.BACKEND)
first_idx = prompt.find("# Your tools are ready")
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