mirror of
https://github.com/usestrix/strix.git
synced 2026-08-16 01:16:40 +02:00
fix(core): tell the parent when an interactive subagent parks
Parking is self-service only for the root, which the user is watching. A parked child owes its parent a report it can no longer send, so the parent would wait out its full timeout for nothing.
This commit is contained in:
@@ -537,6 +537,10 @@ async def _exhausted_recovery(
|
||||
agent_id,
|
||||
)
|
||||
await coordinator.set_status(agent_id, "waiting")
|
||||
# The user talks to the root, so parking is only self-service there. A parked
|
||||
# subagent owes its parent a report it can no longer send, and the parent would
|
||||
# otherwise wait out its full timeout for a message that is never coming.
|
||||
await _notify_parent_on_stall(coordinator, agent_id)
|
||||
return result
|
||||
|
||||
|
||||
@@ -839,6 +843,36 @@ _TERMINAL_NOTICE = {
|
||||
}
|
||||
|
||||
|
||||
_STALL_NOTICE = (
|
||||
"[Agent stalled] {name} ({agent_id}) kept ending turns without a tool call and is "
|
||||
"parked until it receives a message. It will not send a completion report on its "
|
||||
"own: either message it with a concrete next step to unblock it, or stop waiting on "
|
||||
"it and account for its unfinished subtask."
|
||||
)
|
||||
|
||||
|
||||
async def _notify_parent_on_stall(
|
||||
coordinator: AgentCoordinator,
|
||||
agent_id: str,
|
||||
) -> None:
|
||||
"""Tell the parent that a child parked mid-task, so it stops waiting blindly."""
|
||||
async with coordinator._lock:
|
||||
parent = coordinator.parent_of.get(agent_id)
|
||||
name = coordinator.names.get(agent_id, agent_id)
|
||||
if parent is None:
|
||||
return
|
||||
await coordinator.send(
|
||||
parent,
|
||||
{
|
||||
"from": agent_id,
|
||||
"type": "stalled",
|
||||
"priority": "high",
|
||||
"content": _STALL_NOTICE.format(name=name, agent_id=agent_id),
|
||||
},
|
||||
interrupt=False,
|
||||
)
|
||||
|
||||
|
||||
async def _notify_parent_on_terminal(
|
||||
coordinator: AgentCoordinator,
|
||||
agent_id: str,
|
||||
|
||||
@@ -891,6 +891,54 @@ async def test_interactive_recovery_exhaustion_parks_instead_of_crashing(
|
||||
assert coordinator.statuses["root"] == "waiting"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_interactive_subagent_exhaustion_tells_its_parent(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The user only talks to the root, so a parked child must report up.
|
||||
|
||||
Otherwise a parent blocked in wait_for_message burns its whole timeout
|
||||
waiting for a completion report the child can no longer send.
|
||||
"""
|
||||
coordinator = AgentCoordinator()
|
||||
await coordinator.register("root", "strix", parent_id=None)
|
||||
await coordinator.register("child", "recon", parent_id="root")
|
||||
calls: list[Any] = []
|
||||
monkeypatch.setattr(
|
||||
execution,
|
||||
"_run_cycle_parked",
|
||||
_scripted_cycle(coordinator, "child", ["running"], calls),
|
||||
)
|
||||
|
||||
await _drive(coordinator, "child", interactive=True)
|
||||
|
||||
assert coordinator.statuses["child"] == "waiting"
|
||||
pending, items = await coordinator.consume_pending("root", include_items=True)
|
||||
assert pending == 1
|
||||
notice = str(items[0])
|
||||
assert "child" in notice
|
||||
assert "parked" in notice
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_interactive_root_exhaustion_notifies_nobody(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""A parked root is self-service: the user is already watching it."""
|
||||
coordinator = AgentCoordinator()
|
||||
await coordinator.register("root", "strix", parent_id=None)
|
||||
monkeypatch.setattr(
|
||||
execution,
|
||||
"_run_cycle_parked",
|
||||
_scripted_cycle(coordinator, "root", ["running"], []),
|
||||
)
|
||||
|
||||
await _drive(coordinator, "root", interactive=True)
|
||||
|
||||
pending, _ = await coordinator.consume_pending("root")
|
||||
assert pending == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_noninteractive_recovery_exhaustion_crashes(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
Reference in New Issue
Block a user