fix(conductor): surface LLM error to user when conductor falls silent (#342) (#414)

When a single LLM provider goes down mid-task (auth, network, quota,
etc.), the conductor falls completely silent: MixinSession yields
errors as text chunks (not Python exceptions), so the existing
try/except guard in conductor_loop never triggers, and the conductor's
only way to talk to the user (LLM-decided POST /chat tool call) is dead.

Add a tiny watchdog: monitor_conductor_queue now returns the conductor
agent's final done text, and conductor_loop scans the tail-1000-char
window for "!!!Error:" — the prefix llmcore uses for unrecoverable
errors. Tail window (not whole text) avoids false positives from
earlier turns; using a character window (not last-line) matches
ga.do_no_tool's content[-100:] style and tolerates the decoration
(fence + "[Info] Final response to user.") that do_no_tool appends
after the error chunk when its own short window check misses. When
detected, push the matched error line as a system chat message so the
user knows what happened. A dedup check prevents repeated warnings
if multiple events fail in succession.

+16 -4 lines. Only frontends/conductor.py touched. No agent_loop /
llmcore changes; other frontends unaffected.
This commit is contained in:
zhangshuw authored and GitHub committed 2026-05-18 15:26:13 +08:00
1 parent 4c550cbb72
commit 9fda145746
1 file changed
+16 -4
+16 -4
View File
@@ -288,13 +288,14 @@ def _auto_cleanup_loop():
if to_abort:
push_cards()
def monitor_conductor_queue(dq: "queue.Queue"):
"""Conductor output is discarded. Visible reply must go through POST /chat only."""
def monitor_conductor_queue(dq: "queue.Queue") -> str:
"""Block until done. Conductor output is not surfaced to users; the returned
text is for caller-side error detection only (#342)."""
while True:
item = dq.get()
if "done" in item:
print(f"Conductor task done")
break
return item.get("done", "") or ""
def conductor_loop():
global conductor_agent, conductor_started
@@ -321,7 +322,18 @@ def conductor_loop():
prompt = conductor_prompt_from_events(events)
dq = conductor_agent.put_task(prompt, source="conductor")
# Block here until conductor finishes — serializes execution
monitor_conductor_queue(dq)
done_text = monitor_conductor_queue(dq)
# Fallback: if conductor's last turn ended with an LLM error (yielded as
# text by MixinSession instead of raised), surface it directly (#342).
# Use tail-window substring check (same style as ga.do_no_tool's
# content[-100:]); window covers the error line plus any decoration
# (fence + "[Info] Final response to user.") appended by do_no_tool.
tail = (done_text or '')[-1000:]
if '!!!Error:' in tail:
last = chat_messages[-1] if chat_messages else None
if not (last and last.get('role') == 'system' and last.get('msg', '').startswith('⚠ LLM')):
err = next((l for l in reversed(tail.splitlines()) if l.startswith('!!!Error:')), '')
add_chat(f"⚠ LLM 暂不可用:{err[:200]}", role="system")
except Exception as e:
add_chat(f"Conductor error: {e}", role="system")