mirror of
https://github.com/usestrix/strix.git
synced 2026-08-16 09:26:39 +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.
67 lines
2.2 KiB
Python
67 lines
2.2 KiB
Python
"""Tests for the ``respond_to_user`` yield tool."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from agents.tool_context import ToolContext
|
|
|
|
from strix.core.agents import AgentCoordinator
|
|
from strix.tools.respond.tool import respond_to_user
|
|
|
|
|
|
async def _call(context: dict[str, Any], message: str = "here is what I found") -> dict[str, Any]:
|
|
ctx = ToolContext(
|
|
context=context,
|
|
tool_name="respond_to_user",
|
|
tool_call_id="call-1",
|
|
tool_arguments="{}",
|
|
)
|
|
raw = await respond_to_user.on_invoke_tool(ctx, json.dumps({"message": message}))
|
|
return json.loads(raw) # type: ignore[no-any-return]
|
|
|
|
|
|
async def _context(*, interactive: bool, agent_id: str = "root") -> dict[str, Any]:
|
|
coordinator = AgentCoordinator()
|
|
await coordinator.register("root", "strix", parent_id=None)
|
|
return {"coordinator": coordinator, "agent_id": agent_id, "interactive": interactive}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_parks_the_agent_and_carries_the_message() -> None:
|
|
context = await _context(interactive=True)
|
|
result = await _call(context)
|
|
|
|
coordinator = context["coordinator"]
|
|
assert result["success"] is True
|
|
assert result["wait_outcome"] == "waiting"
|
|
assert result["message"] == "here is what I found"
|
|
assert coordinator.statuses["root"] == "waiting"
|
|
# Recorded as a human wait, so the driver never auto-resumes it.
|
|
assert coordinator.wait_kinds["root"] == "user"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rejected_in_an_autonomous_run() -> None:
|
|
context = await _context(interactive=False)
|
|
result = await _call(context)
|
|
|
|
assert result["success"] is False
|
|
assert "finish_scan" in result["error"]
|
|
assert context["coordinator"].statuses["root"] == "running"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_message_that_already_arrived_is_taken_instead_of_parking() -> None:
|
|
context = await _context(interactive=True)
|
|
coordinator = context["coordinator"]
|
|
await coordinator.send("root", {"from": "user", "content": "wait, one more thing"})
|
|
|
|
result = await _call(context)
|
|
|
|
assert result["wait_outcome"] == "message_arrived"
|
|
assert result["pending_messages"] == 1
|
|
assert coordinator.statuses["root"] == "running"
|