[eric] delegation: a child declined by the filter says so to its parent, and a third spawn for a task declined twice this turn is refused; one board re-dispatched 21 children, 14 declined

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ciregenz
2026-09-07 18:32:57 -07:00
co-authored by Claude Fable 5.1
parent 9bdd45f791
commit e4b16bbe07
2 changed files with 138 additions and 2 deletions
+43 -2
View File
@@ -7,7 +7,7 @@ as AgentLaunch."""
import asyncio
import logging
from datetime import datetime
from typing import Dict, Optional
from typing import Dict, List, Optional
from uuid import uuid4
from typeguard import typechecked
@@ -24,6 +24,37 @@ from backend.apps.agents.manager.subagent_budget import (
logger = logging.getLogger(__name__)
# Eric's board, 2026-08-26: one parent spawned 21 transcription children and 14 died on the policy filter, each
# re-dispatched with a rephrased prompt because the return said only "No response from sub-agent."
DECLINED_CHILDREN_CAP = 2
DECLINED_REPLY = (
"The sub-agent's request was declined by the model provider's filter before it finished. Sending the "
"same task to another sub-agent will be declined too, so do not spawn another for it. Tell the user what "
"was completed and what was declined, and let them decide."
)
@typechecked
def declined_children_this_turn(sessions: Dict[str, AgentSession], parent: AgentSession) -> List[AgentSession]:
"""Children of this parent that ended on the filter since the parent's last real message."""
p_asks = [m.timestamp for m in parent.messages if m.role == "user" and not m.hidden]
p_since = p_asks[-1] if p_asks else parent.created_at
return [
s for s in sessions.values()
if s.parent_session_id == parent.id and s.last_failure_kind == "policy_block" and s.created_at >= p_since
]
@typechecked
def child_reply(child: AgentSession) -> str:
if child.last_failure_kind == "policy_block":
from backend.apps.agents.core.error_classify import neutralize_provider_refusal
p_partial = neutralize_provider_refusal(last_assistant_text(child) or "")
return DECLINED_REPLY + (f"\n\nIts last note before stopping: {p_partial}" if p_partial else "")
return last_assistant_text(child) or "No response from sub-agent."
def last_assistant_text(session: AgentSession) -> Optional[str]:
for msg in reversed(session.messages):
if msg.role == "assistant":
@@ -53,6 +84,15 @@ class SpawnAgentRun(AgentManagerProtocol):
raise ValueError(f"Parent session {parent_session_id} not found")
parent = AgentSession(**data)
p_declined = declined_children_this_turn(self.sessions, parent)
if len(p_declined) >= DECLINED_CHILDREN_CAP:
logger.warning(f"SpawnAgent refused for {parent_session_id}: {len(p_declined)} children declined by the filter this turn")
return {"error": (
f"{len(p_declined)} sub-agents for this task were declined by the model provider's filter this turn. "
"Not spawning another: the same task will be declined again. Tell the user what was completed "
"and what was declined, and let them decide."
)}
title = (prompt.strip().splitlines() or [""])[0][:60] or "Sub-agent"
child = AgentSession(
id=uuid4().hex,
@@ -103,6 +143,7 @@ class SpawnAgentRun(AgentManagerProtocol):
await self.run_agent_loop(child.id, p_sent)
return {
"session_id": child.id,
"response": last_assistant_text(child) or "No response from sub-agent.",
"status": child.status,
"response": child_reply(child),
"cost_usd": child.cost_usd,
}
@@ -0,0 +1,95 @@
"""Eric's board, 2026-08-26: a parent on Opus 5 spawned 21 transcription children over an hour; 14 died on the
policy filter and the parent re-dispatched each one with a rephrased prompt, because the return said only
"No response from sub-agent." The child now says it was declined, and a third spawn after two declines is refused."""
import asyncio
from datetime import datetime, timedelta
from pytest import MonkeyPatch
from backend.apps.agents.agent_manager import agent_manager
from backend.apps.agents.core.models import AgentSession, Message
from backend.apps.agents.manager.SpawnAgentRun import DECLINED_CHILDREN_CAP, child_reply, declined_children_this_turn
def p_parent() -> AgentSession:
parent = AgentSession(name="parent", model="opus-5", cwd="/tmp/pw", dashboard_id="dashX")
parent.messages.append(Message(role="user", content="transcribe the book"))
agent_manager.sessions[parent.id] = parent
return parent
def p_declined_child(parent: AgentSession, born: datetime) -> AgentSession:
child = AgentSession(name="child", model="opus-5", mode="sub-agent", parent_session_id=parent.id, status="error", created_at=born)
child.last_failure_kind = "policy_block"
agent_manager.sessions[child.id] = child
return child
def test_a_declined_child_tells_the_parent_not_to_spawn_another(monkeypatch: MonkeyPatch) -> None:
parent = p_parent()
async def blocked_loop(session_id: str, prompt: str, **kwargs: object) -> None:
s = agent_manager.sessions[session_id]
s.messages.append(Message(role="assistant", content="Starting on page 6.", branch_id=s.active_branch_id))
s.status = "error"
s.last_failure_kind = "policy_block"
monkeypatch.setattr(agent_manager, "run_agent_loop", blocked_loop)
result = asyncio.run(agent_manager.spawn_agent(prompt="transcribe pages 6-14", parent_session_id=parent.id))
assert result["status"] == "error"
assert "declined" in result["response"] and "do not spawn another" in result["response"]
assert "Starting on page 6." in result["response"], "the partial note still comes home"
assert "No response from sub-agent" not in result["response"]
def test_a_third_spawn_after_two_declines_this_turn_is_refused(monkeypatch: MonkeyPatch) -> None:
parent = p_parent()
now = datetime.now()
for _ in range(DECLINED_CHILDREN_CAP):
p_declined_child(parent, now + timedelta(seconds=1))
spawned: list[str] = []
async def loop(session_id: str, prompt: str, **kwargs: object) -> None:
spawned.append(session_id)
monkeypatch.setattr(agent_manager, "run_agent_loop", loop)
result = asyncio.run(agent_manager.spawn_agent(prompt="transcribe pages 6-14 again", parent_session_id=parent.id))
assert "error" in result and "declined" in result["error"]
assert spawned == [], "no child is born for a task the filter already declined twice"
def test_declines_before_the_users_latest_message_do_not_count(monkeypatch: MonkeyPatch) -> None:
"""A new ask is a new turn: the user may have changed the task, so the cap resets on their message."""
parent = p_parent()
old = datetime.now() - timedelta(minutes=5)
for _ in range(DECLINED_CHILDREN_CAP):
p_declined_child(parent, old)
parent.messages.append(Message(role="user", content="try a different approach"))
assert declined_children_this_turn(agent_manager.sessions, parent) == []
spawned: list[str] = []
async def loop(session_id: str, prompt: str, **kwargs: object) -> None:
spawned.append(session_id)
agent_manager.sessions[session_id].status = "completed"
monkeypatch.setattr(agent_manager, "run_agent_loop", loop)
result = asyncio.run(agent_manager.spawn_agent(prompt="summarise instead", parent_session_id=parent.id))
assert len(spawned) == 1 and result["status"] == "completed"
def test_a_clean_child_reply_is_unchanged() -> None:
child = AgentSession(name="c", model="opus-5", mode="sub-agent")
child.messages.append(Message(role="assistant", content="9 phonemes: 6 to 14."))
assert child_reply(child) == "9 phonemes: 6 to 14."
assert child_reply(AgentSession(name="e", model="opus-5", mode="sub-agent")) == "No response from sub-agent."
def test_the_stamp_is_set_by_the_policy_card_and_cleared_at_turn_start() -> None:
import inspect
from backend.apps.agents.agent_manager import AgentManager
from backend.apps.agents.manager.run import handle_run_error as hre
src = inspect.getsource(hre.handle_run_error)
assert 'session.last_failure_kind = "policy_block"' in src
assert src.index('session.last_failure_kind = "policy_block"') > src.index("policy_block_sibling("), "only the card path stamps; a failover that continues is not a failure"
assert "session.last_failure_kind = None" in inspect.getsource(AgentManager.run_agent_loop)