@@ -3262,6 +3262,12 @@ async def stream_agent_loop(
32623262 r"\b[^.\n]{0,140}" ,
32633263 re .IGNORECASE ,
32643264 )
3265+ _HOLDING_RE = re .compile (
3266+ r"(?:please\s+)?(?:hold\s+on|wait)\b|one\s+moment\b|just\s+a\s+(?:moment|second|minute)\b|"
3267+ r"\bi\s+am\s+(?:now\s+)?(?:reading|fetching|searching|investigating|scanning|processing)\b|"
3268+ r"\bi'?m\s+(?:now\s+)?(?:reading|fetching|searching|investigating|scanning|processing)\b" ,
3269+ re .IGNORECASE ,
3270+ )
32653271 _awaiting_user = False # set by ask_user → end the turn and wait for a choice
32663272
32673273 # Document streaming state (persists across rounds)
@@ -3819,32 +3825,44 @@ async def stream_agent_loop(
38193825 # tool doesn't pin us in a forever loop.
38203826 _intent_text = _strip_think_blocks (cleaned_round ).strip ()
38213827 _intent_match = _INTENT_RE .search (_intent_text ) if _intent_text else None
3828+ _has_holding = bool (_intent_text and _HOLDING_RE .search (_intent_text ))
38223829 # Only nudge when the round REALLY looks like an unfinished
38233830 # promise: short response (<400 chars), no fenced code/answer,
38243831 # and an action-intent phrase was matched. Long answers that
38253832 # happen to contain "let me know" are not stalls.
3833+ # However, if a holding/waiting pattern is matched, we bypass
3834+ # the length restriction because "please hold" or "I am reading"
3835+ # without tools always stalls the run regardless of response length.
38263836 _looks_like_promise = (
38273837 not guide_only
3828- and _intent_match is not None
3829- and len (_intent_text ) < 400
3838+ and ( _intent_match is not None or _has_holding )
3839+ and ( len (_intent_text ) < 400 or _has_holding )
38303840 and "```" not in _intent_text
38313841 )
38323842 if _looks_like_promise and _intent_nudge_count < _MAX_INTENT_NUDGES :
38333843 _intent_nudge_count += 1
3834- _matched_phrase = _intent_match .group (0 ).strip ()
3835- logger .info (f"[agent] intent-without-action nudge #{ _intent_nudge_count } on round { round_num } : { _matched_phrase !r} " )
3836- _lower_phrase = _matched_phrase .lower ()
3837- _cookbook_log_hint = ""
3838- if any (_word in _lower_phrase for _word in ("log" , "logs" , "output" , "tail" , "status" )):
3839- _cookbook_log_hint = (
3840- " If this is about a Cookbook/model serve, the concrete calls are: "
3841- "`list_served_models` first, then `tail_serve_output` with the "
3842- "session_id from the serve/list result. Never answer with "
3843- "\" check logs\" when those tools are available."
3844+ if _has_holding :
3845+ logger .info (f"[agent] intent-without-action holding pattern nudge #{ _intent_nudge_count } on round { round_num } " )
3846+ _nudge_msg = (
3847+ "You wrote a holding/waiting pattern or stated that you are currently performing "
3848+ "an action (e.g. reading, searching, wait, hold on) but ended the turn without making any tool calls. "
3849+ "In this environment, you must emit the tool calls (such as `read_email` with the relevant UIDs) "
3850+ "directly in the same turn so they can execute. You cannot 'hold' or 'wait' for a future turn "
3851+ "without calling a tool first. Call the actual tools now to continue the task."
38443852 )
3845- messages .append ({
3846- "role" : "system" ,
3847- "content" : (
3853+ else :
3854+ _matched_phrase = _intent_match .group (0 ).strip ()
3855+ logger .info (f"[agent] intent-without-action nudge #{ _intent_nudge_count } on round { round_num } : { _matched_phrase !r} " )
3856+ _lower_phrase = _matched_phrase .lower ()
3857+ _cookbook_log_hint = ""
3858+ if any (_word in _lower_phrase for _word in ("log" , "logs" , "output" , "tail" , "status" )):
3859+ _cookbook_log_hint = (
3860+ " If this is about a Cookbook/model serve, the concrete calls are: "
3861+ "`list_served_models` first, then `tail_serve_output` with the "
3862+ "session_id from the serve/list result. Never answer with "
3863+ "\" check logs\" when those tools are available."
3864+ )
3865+ _nudge_msg = (
38483866 f"You just wrote: \" { _matched_phrase } \" — but ended the "
38493867 "turn without making the actual tool call. The user can "
38503868 "see you announced the action but didn't run it, which "
@@ -3853,7 +3871,11 @@ async def stream_agent_loop(
38533871 f"{ _cookbook_log_hint } "
38543872 "If you decided not to do it after all, say so plainly in "
38553873 "one sentence instead of restating the plan."
3856- ),
3874+ )
3875+
3876+ messages .append ({
3877+ "role" : "system" ,
3878+ "content" : _nudge_msg ,
38573879 })
38583880 # Visible signal in the stream so the user knows we caught it.
38593881 yield f'data: { json .dumps ({"type" : "agent_step" , "round" : round_num + 1 })} \n \n '
0 commit comments