"""`/btw` 命令:side question — 不打断主 Agent 的临时 subagent 问答。 - 持锁 deepcopy backend.history → 后台线程 backend.raw_ask 单次拉答 - 主 agent backend.history 零写入;不入 task_queue - 答案 → display_queue 'done'(install 路径)或同步 return(frontend 路径) 复用 backend.raw_ask + make_messages,不新建 LLM 实例。 """ from __future__ import annotations import copy, os, threading, time from typing import Optional _WRAPPER_ZH = """ 这是用户的临时插问 (side question)。主 agent 仍在后台运行,**不会被打断**。 身份与边界: - 你是一个独立的轻量 sub-agent - 上下文里能看到主 agent 与用户的完整对话、最近的工具调用与结果 - 用户在问当前进展或顺便确认某事——基于已有信息**一次性**作答 - 没有任何工具可用:不要"让我查一下" / "我去试试" / 任何承诺动作 - 信息不足就坦白说"基于目前对话我不知道" 侧问内容如下: {question}""" _WRAPPER_EN = """ This is a side question from the user. The main agent is NOT interrupted — it continues in the background. Identity & boundaries: - You are an independent lightweight sub-agent - You can see the full conversation between the main agent and the user, plus recent tool calls/results - The user is asking about current progress or a quick aside — answer in **one shot** from existing info - You have NO tools — never say "let me check" / "I'll try" / any action promise - If info is missing, just say "based on the conversation I don't know" Question: {question}""" _TIMEOUT_SEC = 120 def _wrapper(): return _WRAPPER_EN if os.environ.get('GA_LANG') == 'en' else _WRAPPER_ZH def _strip_cmd(query): s = (query or '').strip() return s[len('/btw'):].strip() if s.startswith('/btw') else s def _help_text(): return ('**/btw 用法**:side question — 临时问主 agent 当前进展,不打断主线\n\n' '`/btw <你的问题>`\n\n' '行为:抓取当前对话上下文 → 单轮纯文本作答(无工具)→ 主 agent 历史不变。') def _snapshot_history(backend): """Lock + deepcopy: defends against concurrent compress_history_tags mutating inner blocks.""" with backend.lock: return copy.deepcopy(list(backend.history)) def _build_wire(backend, history, sidequest_msg): """history + sidequest → wire-format. Dispatches: BaseSession subclasses → make_messages, Native* → raw pairs (raw_ask runs _fix/_drop/_ensure transforms itself).""" msgs = history + [sidequest_msg] if hasattr(backend, 'make_messages'): return backend.make_messages(msgs) return [{"role": m["role"], "content": list(m.get("content", []))} for m in msgs] def _ask(agent, question, deadline): """One-shot raw_ask against current backend; never mutates backend.history.""" backend = agent.llmclient.backend user_msg = {"role": "user", "content": [{"type": "text", "text": _wrapper().format(question=question)}]} wire = _build_wire(backend, _snapshot_history(backend), user_msg) text = '' for chunk in backend.raw_ask(wire): text += chunk if time.time() > deadline: return text + '\n\n⚠️ /btw 超时,仅返回部分回复。' return text def _format(question, body, took): head = f'> 🟡 /btw {question}\n\n' return head + (body.strip() or '*(空回复)*') + f'\n\n*({took:.1f}s)*' def _run(agent, question, deadline): """Catches errors at the boundary so neither caller path needs its own try/except.""" try: return _ask(agent, question, deadline) except Exception as e: return f'❌ /btw 失败: {type(e).__name__}: {e}' def handle(agent, query, display_queue) -> Optional[str]: """Slash-cmd entry (server-side, install path). Spawn worker; return None to consume.""" question = _strip_cmd(query) if not question or question in ('help', '?', '-h', '--help'): display_queue.put({'done': _help_text(), 'source': 'system'}) return None started = time.time() deadline = started + _TIMEOUT_SEC def worker(): body = _run(agent, question, deadline) display_queue.put({'done': _format(question, body, time.time() - started), 'source': 'system'}) threading.Thread(target=worker, daemon=True, name='btw-sidequest').start() return None def handle_frontend_command(agent, query) -> str: """Sync entry for frontends wanting a string back (tg/wx/stapp/...).""" question = _strip_cmd(query) if not question or question in ('help', '?', '-h', '--help'): return _help_text() started = time.time() body = _run(agent, question, started + _TIMEOUT_SEC) return _format(question, body, time.time() - started) def install(cls): """Idempotent monkey-patch: intercept /btw before original dispatch.""" orig = cls._handle_slash_cmd if getattr(orig, '_btw_patched', False): return def patched(self, raw_query, display_queue): s = (raw_query or '').strip() if s == '/btw' or s.startswith('/btw ') or s.startswith('/btw\t'): r = handle(self, raw_query, display_queue) if r is None: return None return r return orig(self, raw_query, display_queue) patched._btw_patched = True cls._handle_slash_cmd = patched