[eric] agents: silent-quit re-nudges are earned by new tool work, capped at 3, stalls surface honestly (ENG-121 family)

This commit is contained in:
ciregenz
2026-08-05 20:10:54 -07:00
parent 1a1bfd0957
commit b6f8fbb5ef
4 changed files with 58 additions and 14 deletions
+2
View File
@@ -135,6 +135,8 @@ class AgentSession(BaseModel):
pending_continuation_prompt: Optional[str] = None
# Silent-quit nudges spent since the user's last real message; capped at 1 so an agent that keeps ending empty can't loop.
empty_finish_nudges: int = 0
# Tool-call count at the last nudge: a re-nudge is only earned by NEW tool work since then.
empty_finish_progress_mark: int = 0
# Sanitized server names model has explicitly activated this session; _build_mcp_servers intersects connected MCPs with this. Non-bypassable; dispatch-layer gate.
active_mcps: list[str] = Field(default_factory=list)
# Heuristic preamble tokens (preset + tool defs + MCP descs + composed prompt); subtracted from displayed input.
+1
View File
@@ -136,6 +136,7 @@ class Messaging(AgentManagerProtocol):
# A real user message opens a fresh silent-quit budget; the cap only guards within one ask.
if not hidden:
session.empty_finish_nudges = 0
session.empty_finish_progress_mark = 0
# Fire a background aux LLM call to generate a 3-6 word verb-phrase describing this turn ("Auditing the pull request", "Drafting your email"). The narrator pill swaps from its heuristic verb to this label as soon as it lands, usually ~500ms-1s into the turn, which is exactly when "Thinking…" starts feeling generic. Provider-agnostic via resolve_aux_model. Non-blocking; failure is silent and the heuristic stays.
if not hidden and prompt:
try:
@@ -20,14 +20,23 @@ NUDGE_PROMPT = (
logger = logging.getLogger(__name__)
NUDGE_HARD_CAP = 3
@typechecked
def maybe_nudge_empty_finish(session: AgentSession, session_id: str) -> bool:
"""Arm one hidden continue nudge when the finished turn quit silently; the loop's existing
auto-continuation block dispatches it. Capped per user ask; never loops."""
if getattr(session, "pending_continuation", False) or session.empty_finish_nudges >= 1:
"""Arm a hidden continue nudge when the finished turn quit silently; the loop's existing
auto-continuation block dispatches it. A re-nudge must be EARNED by new tool work since the
last one (the model is visibly still working, just mute); a stalled continuation surfaces
honestly, so this can never ping-pong a model that has nothing left to do."""
if getattr(session, "pending_continuation", False) or session.empty_finish_nudges >= NUDGE_HARD_CAP:
return False
if not turn_finished_empty(session):
return False
p_tool_calls = p_count_tool_calls(session)
if session.empty_finish_nudges >= 1 and p_tool_calls <= session.empty_finish_progress_mark:
return False
session.empty_finish_progress_mark = p_tool_calls
session.empty_finish_nudges += 1
session.pending_continuation = True
session.pending_continuation_prompt = NUDGE_PROMPT
@@ -43,6 +52,11 @@ def maybe_nudge_empty_finish(session: AgentSession, session_id: str) -> bool:
P_ANSWER_TOOL_MARKERS = ("openswarm-ui", "ShowUI", "AskUI", "AskUserQuestion")
@typechecked
def p_count_tool_calls(session: AgentSession) -> int:
return sum(1 for m in get_branch_messages(session) if getattr(m, "role", "") == "tool_call")
def p_tool_name_of(msg: object) -> str:
content = getattr(msg, "content", None)
if isinstance(content, dict):