mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-14 05:37:40 +02:00
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wtspwSFzZmjCx9UNPAorQ
100 lines
3.9 KiB
Python
100 lines
3.9 KiB
Python
"""One invariant at the single exit: a finished turn always left the user something to read.
|
|
|
|
Every silent-stop fix so far has been a DETECTOR: empty finish, vanishing quit, stalled nudge,
|
|
produced-nothing, exhausted budget. Each one enumerates a shape somebody already found in the
|
|
field, which means the next shape nobody has found yet still ships as silence. That is the wrong
|
|
tier, and the ENG-354 history is the proof: the class kept coming back wearing a different hat.
|
|
|
|
So this does not classify anything. It asks the only question the user asks, at the one place every
|
|
terminal path passes through:
|
|
|
|
since you last spoke to me, has anything appeared that I can read?
|
|
|
|
If not, the honest line goes in. It does not matter whether the cause was a silent quit, a shape
|
|
nobody has named, or a bug written next year: the state "turn ended, nothing to read" stops being
|
|
representable, rather than being caught case by case.
|
|
|
|
Deliberately last: the detectors upstream produce BETTER messages because they know why. This only
|
|
fires when every one of them declined, so it is a floor, never a replacement.
|
|
"""
|
|
|
|
import logging
|
|
from typing import List
|
|
|
|
from typeguard import typechecked
|
|
|
|
from backend.apps.agents.core.models import AgentSession, Message
|
|
from backend.apps.agents.manager.run.empty_finish import (
|
|
EXHAUSTED_NOTE,
|
|
EXHAUSTED_NOTE_NO_PROGRESS,
|
|
P_ANSWER_TOOL_MARKERS,
|
|
turn_showed_work,
|
|
)
|
|
from backend.apps.agents.manager.session.history_compaction import get_branch_messages
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# States where the turn is genuinely over. "stopped" is excluded on purpose: the user pressed stop,
|
|
# so they know exactly why it ended and do not need to be told.
|
|
P_TERMINAL = ("completed", "error")
|
|
|
|
|
|
@typechecked
|
|
def p_tool_name_of(msg: object) -> str:
|
|
content = getattr(msg, "content", None)
|
|
if isinstance(content, dict):
|
|
return str(content.get("tool") or content.get("tool_name") or "")
|
|
return ""
|
|
|
|
|
|
@typechecked
|
|
def turn_left_the_user_with_nothing(session: AgentSession) -> bool:
|
|
"""True when nothing readable has appeared since the user's last visible message."""
|
|
msgs: List = [m for m in get_branch_messages(session) if not getattr(m, "hidden", False)]
|
|
if not msgs:
|
|
return False
|
|
|
|
p_last_user = -1
|
|
for i, m in enumerate(msgs):
|
|
if getattr(m, "role", "") == "user":
|
|
p_last_user = i
|
|
if p_last_user < 0:
|
|
return False
|
|
|
|
for m in msgs[p_last_user + 1:]:
|
|
role = getattr(m, "role", "")
|
|
if role == "system":
|
|
return False
|
|
if role == "assistant":
|
|
text = m.content if isinstance(m.content, str) else ""
|
|
if text.strip():
|
|
return False
|
|
if role == "tool_call" and any(mk in p_tool_name_of(m) for mk in P_ANSWER_TOOL_MARKERS):
|
|
# A rendered widget IS the answer; the user is looking at it.
|
|
return False
|
|
return True
|
|
|
|
|
|
@typechecked
|
|
def ensure_turn_spoke(session: AgentSession, session_id: str) -> bool:
|
|
"""Append the honest line if the turn is over and said nothing. Returns whether it fired."""
|
|
if session.status not in P_TERMINAL:
|
|
return False
|
|
# A parked or continuing turn is not over; speaking now would be the lie.
|
|
if getattr(session, "pending_continuation", False) or getattr(session, "awaiting_reconnect", False):
|
|
return False
|
|
if not turn_left_the_user_with_nothing(session):
|
|
return False
|
|
|
|
# Point at work only when there is work to point at; a turn that did nothing gets the copy
|
|
# that names an action the user can actually take.
|
|
p_note = EXHAUSTED_NOTE if turn_showed_work(session) else EXHAUSTED_NOTE_NO_PROGRESS
|
|
session.messages.append(
|
|
Message(role="system", content=p_note, branch_id=session.active_branch_id)
|
|
)
|
|
logger.warning(
|
|
f"Agent {session_id}: turn ended with nothing readable and no detector claimed it; "
|
|
"the floor spoke instead of leaving the user with a bare Done"
|
|
)
|
|
return True
|