fix desktop empty turn handling
This commit is contained in:
1 parent
eacd3d176d
commit
58cb4cdc7c
2 files changed
+61
-13
No files matched your search
+56
-10
@@ -86,12 +86,32 @@ def find_default_ga_root() -> Path:
|
||||
DEFAULT_GA_ROOT = find_default_ga_root()
|
||||
|
||||
_FINAL_INFO_RE = re.compile(r'\n*`{5}\n*\[Info\] Final response to user\.\n*`{5}\s*$')
|
||||
_RUNNING_MARKER_RE = re.compile(
|
||||
r'^\s*\*{0,2}(?:LLM Running\s*\(Turn\s*\d+\)|Turn\s*\d+)\s*\.\.\.\*{0,2}\s*$',
|
||||
re.IGNORECASE | re.MULTILINE,
|
||||
)
|
||||
_SUMMARY_RE = re.compile(r'<summary>[\s\S]*?</summary>', re.IGNORECASE)
|
||||
_TOOL_TRANSCRIPT_RE = re.compile(
|
||||
r'(?ms)^\s*.*?Tool:\s*`.*?`{4}[^\n]*\n.*?`{4}\s*(?:`{5}.*?`{5}\s*)?'
|
||||
)
|
||||
|
||||
|
||||
def strip_final_info_marker(text: Any) -> str:
|
||||
return _FINAL_INFO_RE.sub('', str(text or ''))
|
||||
|
||||
|
||||
def strip_non_user_visible_text(text: Any) -> str:
|
||||
cleaned = strip_final_info_marker(text)
|
||||
cleaned = _RUNNING_MARKER_RE.sub('', cleaned)
|
||||
cleaned = _SUMMARY_RE.sub('', cleaned)
|
||||
cleaned = _TOOL_TRANSCRIPT_RE.sub('', cleaned)
|
||||
return cleaned.strip()
|
||||
|
||||
|
||||
def has_user_visible_text(text: Any) -> bool:
|
||||
return bool(strip_non_user_visible_text(text))
|
||||
|
||||
|
||||
def normalize_final_turn_segs(full: str, outputs: Any) -> Optional[List[str]]:
|
||||
if not outputs or not isinstance(outputs, (list, tuple)):
|
||||
return None
|
||||
@@ -133,6 +153,7 @@ class Session:
|
||||
agent: Any = None
|
||||
thread: Optional[threading.Thread] = None
|
||||
cancel_requested: bool = False
|
||||
active_turn_id: str = ""
|
||||
last_error: str = ""
|
||||
pinned: bool = False
|
||||
untitled: bool = True
|
||||
@@ -749,19 +770,25 @@ class AgentManager:
|
||||
if image_metas:
|
||||
extra["images"] = image_metas
|
||||
user_msg = self.add_message(sess, "user", prompt, **extra)
|
||||
turn_id = uuid.uuid4().hex
|
||||
sess.status = "running"
|
||||
sess.cancel_requested = False
|
||||
sess.active_turn_id = turn_id
|
||||
sess.last_error = ""
|
||||
sess.partial = {"id": sess.msg_seq + 1, "role": "assistant", "content": "", "ts": time.time(), "partial": True,
|
||||
"curr_turn": 0, "turn_segs": []} # turn_segs[i]=第i轮全文(权威结构化,前端按轮渲染);content保留双轨兜底
|
||||
t = threading.Thread(target=self.run_agent_turn, args=(sess, prompt, None), daemon=True, name=f"Turn-{sid}")
|
||||
t = threading.Thread(target=self.run_agent_turn, args=(sess, prompt, None, turn_id), daemon=True, name=f"Turn-{sid}")
|
||||
sess.thread = t
|
||||
t.start()
|
||||
seq = sess.msg_seq
|
||||
emit_session_state(sess, "running")
|
||||
return {"ok": True, "sessionId": sid, "accepted": True, "userMessageId": user_msg["id"], "seq": seq}
|
||||
|
||||
def run_agent_turn(self, sess: Session, prompt: str, images: Optional[list] = None):
|
||||
def run_agent_turn(self, sess: Session, prompt: str, images: Optional[list] = None, turn_id: str = ""):
|
||||
def turn_state() -> tuple[bool, bool]:
|
||||
with self.lock:
|
||||
return sess.active_turn_id == turn_id, sess.cancel_requested
|
||||
|
||||
try:
|
||||
if sess.agent is None:
|
||||
sess.agent = self.make_agent(sess)
|
||||
@@ -778,7 +805,10 @@ class AgentManager:
|
||||
pieces = []
|
||||
import queue as _queue
|
||||
while True:
|
||||
if sess.cancel_requested:
|
||||
is_current, is_cancelled = turn_state()
|
||||
if not is_current:
|
||||
return
|
||||
if is_cancelled:
|
||||
break
|
||||
try:
|
||||
item = display_q.get(timeout=1.0)
|
||||
@@ -789,7 +819,7 @@ class AgentManager:
|
||||
text = str(item["next"])
|
||||
pieces.append(text)
|
||||
with self.lock:
|
||||
if sess.partial is not None:
|
||||
if sess.partial is not None and sess.active_turn_id == turn_id:
|
||||
sess.partial["content"] = "".join(pieces) if getattr(agent, "inc_out", False) else text
|
||||
sess.partial["ts"] = time.time()
|
||||
sess.updated_at = time.time()
|
||||
@@ -810,7 +840,7 @@ class AgentManager:
|
||||
done_outputs = normalize_final_turn_segs(full, item.get("outputs")) # done时=turn_resps.copy()全量轮
|
||||
if done_outputs:
|
||||
with self.lock:
|
||||
if sess.partial is not None:
|
||||
if sess.partial is not None and sess.active_turn_id == turn_id:
|
||||
sess.partial["content"] = full
|
||||
sess.partial["ts"] = time.time()
|
||||
sess.partial["updatedAt"] = sess.partial["ts"] if "updatedAt" in sess.partial else sess.partial.get("updatedAt")
|
||||
@@ -821,23 +851,34 @@ class AgentManager:
|
||||
else:
|
||||
pieces.append(str(item))
|
||||
if not full and pieces:
|
||||
full = pieces[-1] if not getattr(agent, "inc_out", False) else "".join(pieces)
|
||||
candidate = pieces[-1] if not getattr(agent, "inc_out", False) else "".join(pieces)
|
||||
if has_user_visible_text(candidate):
|
||||
full = candidate
|
||||
else:
|
||||
full = "GenericAgent object has no put_task method"
|
||||
if not full:
|
||||
full = "(completed)"
|
||||
if sess.cancel_requested:
|
||||
is_current, is_cancelled = turn_state()
|
||||
if not is_current:
|
||||
return
|
||||
if is_cancelled:
|
||||
with self.lock:
|
||||
sess.partial = None
|
||||
if sess.active_turn_id == turn_id:
|
||||
sess.active_turn_id = ""
|
||||
# Ensure status stays cancelled (don't overwrite)
|
||||
if sess.status != "cancelled":
|
||||
sess.status = "cancelled"
|
||||
sess.updated_at = time.time()
|
||||
emit_session_state(sess, "cancelled")
|
||||
return
|
||||
if not full:
|
||||
raise RuntimeError("Agent turn ended without a user-visible response.")
|
||||
with self.lock:
|
||||
if sess.active_turn_id != turn_id:
|
||||
return
|
||||
sess.partial = None
|
||||
full = strip_final_info_marker(full)
|
||||
if not has_user_visible_text(full):
|
||||
raise RuntimeError("Agent turn ended without a user-visible response.")
|
||||
import plan_state
|
||||
plan_state.sync_plan_path_from_text(sess, full, sess.cwd or self.ga_root)
|
||||
# 轨道2: 落库时带结构化全量轮(权威turn_segs),前端按轮渲染;content保留兜底
|
||||
@@ -849,13 +890,17 @@ class AgentManager:
|
||||
try: sess.llm_history = json.loads(json.dumps(agent.llmclient.backend.history, ensure_ascii=False, default=str))
|
||||
except Exception: pass
|
||||
sess.status = "idle"
|
||||
sess.active_turn_id = ""
|
||||
sess.last_error = ""
|
||||
emit_session_state(sess, "idle")
|
||||
except Exception as e:
|
||||
tb = traceback.format_exc()
|
||||
with self.lock:
|
||||
if sess.active_turn_id != turn_id:
|
||||
return
|
||||
sess.partial = None
|
||||
sess.status = "error"
|
||||
sess.active_turn_id = ""
|
||||
sess.last_error = str(e)
|
||||
self.add_message(sess, "error", str(e))
|
||||
print(tb, file=sys.stderr)
|
||||
@@ -905,9 +950,10 @@ class AgentManager:
|
||||
partial_text = ""
|
||||
if sess.partial:
|
||||
partial_text = (sess.partial.get("content") or "").strip()
|
||||
if partial_text:
|
||||
if partial_text and has_user_visible_text(partial_text):
|
||||
self.add_message(sess, "assistant", partial_text, stopped=True)
|
||||
sess.status = "cancelled"
|
||||
sess.active_turn_id = ""
|
||||
sess.partial = None
|
||||
sess.updated_at = time.time()
|
||||
emit_session_state(sess, "cancelled")
|
||||
|
||||
@@ -463,9 +463,11 @@ class GenericAgentHandler(BaseHandler):
|
||||
二次确认仅在回复几乎只包含<thinking>/<summary>和一段大代码块时触发。'''
|
||||
content = getattr(response, 'content', '') or ""
|
||||
thinking = getattr(response, 'thinking', '') or ""
|
||||
if not response or (not content.strip() and not thinking.strip()):
|
||||
yield "[Warn] LLM returned an empty response. Retrying...\n"
|
||||
return self._retry_or_exit("[System] Blank response, regenerate and tooluse")
|
||||
visible_content = re.sub(r"<thinking>[\s\S]*?</thinking>", "", content, flags=re.IGNORECASE)
|
||||
visible_content = re.sub(r"<summary>[\s\S]*?</summary>", "", visible_content, flags=re.IGNORECASE)
|
||||
if not response or not visible_content.strip():
|
||||
yield "[Warn] LLM returned no user-visible response. Retrying...\n"
|
||||
return self._retry_or_exit("[System] Blank response. Regenerate with a user-visible answer or call a tool.")
|
||||
if '[!!! 流异常中断' in content[-100:] or '!!!Error:' in content[-100:]:
|
||||
return self._retry_or_exit("[System] Incomplete response. Regenerate and tooluse.")
|
||||
if 'max_tokens !!!]' in content[-100:]:
|
||||
|
||||
Reference in new issue
Block a user