mirror of
https://github.com/usestrix/strix.git
synced 2026-08-17 01:29:42 +02:00
One tool was doing three jobs (wait on the user, wait on other agents, and - wrongly - wait for a long-running command), so the driver had to guess which one an agent meant and used parent_id as the proxy: the root waits for a human, everyone else waits for agents. That proxy is wrong, since the user can message any agent from the TUI's agent tree. Tool identity now carries the intent, and the coordinator records it as a wait_kind that survives snapshot/restore: respond_to_user -> wait_kind="user", never auto-resumed (root or not) wait_for_agents -> wait_kind="agents", auto-resumed on a 300s timer recovery exhaust -> wait_kind="stalled" respond_to_user fuses the message and the yield into one call, so there is no way to answer and then forget to stop - the two-step that gpt-4o-mini skipped 2/2 in live testing. Plain text still renders as before. Auto-resume is also bounded now: an agent that re-parks after every timeout burned a model turn every 300s for the rest of the scan (and, since parked children notify their parent, spammed the parent's inbox on the same cycle). After _MAX_IDLE_AUTO_RESUMES it stays parked until a real message arrives.
115 lines
3.5 KiB
Python
115 lines
3.5 KiB
Python
"""Tests for scan-agent tool registration in factory."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import pytest
|
|
from agents.tool import FunctionTool
|
|
|
|
from strix.agents import factory
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from agents.tool_context import ToolContext
|
|
|
|
|
|
def _tool(name: str) -> FunctionTool:
|
|
# A per-tool closure keeps two same-named tools unequal, which is what the
|
|
# duplicate-name tests exercise.
|
|
async def invoke(_ctx: ToolContext[Any], _input: str) -> str:
|
|
return "ok"
|
|
|
|
return FunctionTool(
|
|
name=name,
|
|
description="test tool",
|
|
params_json_schema={"type": "object", "properties": {}, "additionalProperties": False},
|
|
on_invoke_tool=invoke,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_registry() -> object:
|
|
saved = list(factory._EXTRA_TOOLS)
|
|
factory._EXTRA_TOOLS.clear()
|
|
try:
|
|
yield
|
|
finally:
|
|
factory._EXTRA_TOOLS[:] = saved
|
|
|
|
|
|
def test_register_agent_tools_is_deduped() -> None:
|
|
tool = _tool("dup")
|
|
factory.register_agent_tools(tool)
|
|
factory.register_agent_tools(tool)
|
|
assert factory.registered_agent_tools() == (tool,)
|
|
|
|
|
|
def test_registered_tools_appear_before_lifecycle_tool() -> None:
|
|
tool = _tool("extra")
|
|
factory.register_agent_tools(tool)
|
|
|
|
root = factory.build_strix_agent(is_root=True)
|
|
child = factory.build_strix_agent(is_root=False)
|
|
|
|
root_names = [t.name for t in root.tools]
|
|
child_names = [t.name for t in child.tools]
|
|
|
|
assert root_names[-2:] == ["extra", "finish_scan"]
|
|
assert child_names[-2:] == ["extra", "agent_finish"]
|
|
|
|
|
|
def test_per_call_extra_tools_stack_with_registry() -> None:
|
|
factory.register_agent_tools(_tool("registered"))
|
|
|
|
agent = factory.build_strix_agent(is_root=True, extra_tools=[_tool("per_call")])
|
|
names = [t.name for t in agent.tools]
|
|
|
|
assert "registered" in names
|
|
assert "per_call" in names
|
|
assert names[-1] == "finish_scan"
|
|
|
|
|
|
def test_register_agent_tools_rejects_duplicate_names() -> None:
|
|
factory.register_agent_tools(_tool("same_name"))
|
|
|
|
with pytest.raises(ValueError, match="same_name"):
|
|
factory.register_agent_tools(_tool("same_name"))
|
|
|
|
|
|
def test_per_call_extra_tools_reject_duplicate_registered_names() -> None:
|
|
factory.register_agent_tools(_tool("same_name"))
|
|
|
|
with pytest.raises(ValueError, match="same_name"):
|
|
factory.build_strix_agent(is_root=True, extra_tools=[_tool("same_name")])
|
|
|
|
|
|
def test_instructions_override_is_used_verbatim() -> None:
|
|
custom = "You are a scan agent. Follow the provided scope."
|
|
|
|
agent = factory.build_strix_agent(is_root=True, instructions_override=custom)
|
|
|
|
assert agent.instructions == custom
|
|
|
|
|
|
def test_no_override_renders_builtin_prompt() -> None:
|
|
agent = factory.build_strix_agent(is_root=True)
|
|
|
|
assert isinstance(agent.instructions, str)
|
|
assert agent.instructions != ""
|
|
|
|
|
|
def test_respond_to_user_is_interactive_only() -> None:
|
|
"""Yielding to the user is meaningless when no user is attached."""
|
|
interactive = factory.build_strix_agent(is_root=True, interactive=True)
|
|
autonomous = factory.build_strix_agent(is_root=True, interactive=False)
|
|
|
|
assert "respond_to_user" in [t.name for t in interactive.tools]
|
|
assert "respond_to_user" not in [t.name for t in autonomous.tools]
|
|
|
|
|
|
def test_wait_for_agents_is_available_in_both_modes() -> None:
|
|
for interactive in (True, False):
|
|
agent = factory.build_strix_agent(is_root=True, interactive=interactive)
|
|
assert "wait_for_agents" in [t.name for t in agent.tools]
|