From da40ba413bb702b4187a0def3858bf130b7b899b Mon Sep 17 00:00:00 2001 From: Liang Jiaqing Date: Fri, 3 Apr 2026 10:43:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20fold=5Fturns=20UI=E6=8A=98=E5=8F=A0=20+?= =?UTF-8?q?=20=E6=94=BE=E5=AE=BD=E8=BE=93=E5=87=BA=E9=99=90=E5=88=B6=20+?= =?UTF-8?q?=20=E5=8E=8B=E7=BC=A9history/key=5Finfo=E6=A0=87=E7=AD=BE=20+?= =?UTF-8?q?=20context=5Fwin=E8=B0=83=E5=A4=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontends/stapp.py | 27 ++++++++++++++++++++++++--- ga.py | 8 ++++---- llmcore.py | 25 ++++++++++++------------- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/frontends/stapp.py b/frontends/stapp.py index 02bddf9..8409a7c 100644 --- a/frontends/stapp.py +++ b/frontends/stapp.py @@ -66,6 +66,23 @@ def render_sidebar(): st.caption("🔴 自主行动已停止") with st.sidebar: render_sidebar() +def fold_turns(text): + parts = re.split(r'(\**LLM Running \(Turn \d+\) \.\.\.*\**)', text) + if len(parts) < 4: return text + result = parts[0] + turns = [] + for i in range(1, len(parts), 2): + marker = parts[i].strip('*') + content = parts[i+1] if i+1 < len(parts) else '' + turns.append((marker, content)) + for idx, (marker, content) in enumerate(turns): + if idx < len(turns) - 1: + m = re.search(r'\s*(.*?)\|*', content, re.DOTALL) + title = m.group(1).strip() if m else marker + result += f'
{title}\n\n{content}\n
\n\n' + else: + result += marker + content + return result def agent_backend_stream(prompt): display_queue = agent.put_task(prompt, source="user") @@ -84,7 +101,11 @@ def agent_backend_stream(prompt): if "messages" not in st.session_state: st.session_state.messages = [] for msg in st.session_state.messages: - with st.chat_message(msg["role"]): st.markdown(msg["content"], unsafe_allow_html=False) + with st.chat_message(msg["role"]): + if msg["role"] == "assistant": + st.markdown(fold_turns(msg["content"]), unsafe_allow_html=True) + else: + st.markdown(msg["content"], unsafe_allow_html=False) # IME composition fix (macOS only) - prevents Enter from submitting during CJK input if os.name != 'nt': @@ -99,8 +120,8 @@ if prompt := st.chat_input("请输入指令"): message_placeholder = st.empty() response = '' for response in agent_backend_stream(prompt): - message_placeholder.markdown(response + "▌", unsafe_allow_html=False) - message_placeholder.markdown(response, unsafe_allow_html=False) + message_placeholder.markdown(fold_turns(response) + "...", unsafe_allow_html=True) + message_placeholder.markdown(fold_turns(response), unsafe_allow_html=True) st.session_state.messages.append({"role": "assistant", "content": response}) st.session_state.last_reply_time = int(time.time()) diff --git a/ga.py b/ga.py index 4e6f30a..4e3cfc8 100644 --- a/ga.py +++ b/ga.py @@ -73,12 +73,12 @@ def code_run(code, code_type="python", timeout=60, cwd=None, code_cwd=None, stop status = "success" if exit_code == 0 else "error" status_icon = "✅" if exit_code == 0 else "❌" if exit_code is None: status_icon = "⏳" - output_snippet = smart_format(stdout_str, max_str_len=600, omit_str='\n[omitted long output]\n') + output_snippet = smart_format(stdout_str, max_str_len=600, omit_str='\n\n[omitted long output]\n\n') yield f"[Status] {status_icon} Exit Code: {exit_code}\n[Stdout]\n{output_snippet}\n" if process.stdout: threading.Thread(target=process.stdout.close, daemon=True).start() return { "status": status, - "stdout": smart_format(stdout_str, max_str_len=8000, omit_str='\n[omitted long output]\n'), + "stdout": smart_format(stdout_str, max_str_len=10000, omit_str='\n\n[omitted long output]\n\n'), "exit_code": exit_code } except Exception as e: @@ -137,7 +137,7 @@ def web_scan(tabs_only=False, switch_tab_id=None, text_only=False): "active_tab": driver.default_session_id } } - if not tabs_only: result["content"] = get_html(driver, cutlist=True, maxchars=28000, text_only=text_only) + if not tabs_only: result["content"] = get_html(driver, cutlist=True, maxchars=38000, text_only=text_only) return result except Exception as e: return {"status": "error", "msg": format_error(e)} @@ -354,7 +354,7 @@ class GenericAgentHandler(BaseHandler): except: pass yield f"JS 执行结果:\n{smart_format(result)}\n" next_prompt = self._get_anchor_prompt(skip=args.get('_index', 0) > 0) - return StepOutcome(smart_format(result, max_str_len=5000), next_prompt=next_prompt) + return StepOutcome(smart_format(result, max_str_len=8000), next_prompt=next_prompt) def do_file_patch(self, args, response): path = self._get_abs_path(args.get("path", "")) diff --git a/llmcore.py b/llmcore.py index c2e58d6..f91e078 100644 --- a/llmcore.py +++ b/llmcore.py @@ -15,32 +15,31 @@ proxy = mykeys.get("proxy", 'http://127.0.0.1:2082') proxies = {"http": proxy, "https": proxy} if proxy else None def compress_history_tags(messages, keep_recent=10, max_len=800): - """Compress // tags in older messages to save tokens. - Supports both prompt-style (ClaudeSession/LLMSession) and content-style (NativeClaudeSession) messages.""" + """Compress // tags in older messages to save tokens.""" compress_history_tags._cd = getattr(compress_history_tags, '_cd', 0) + 1 if compress_history_tags._cd % 5 != 0: return messages _before = sum(len(json.dumps(m, ensure_ascii=False)) for m in messages) _pats = {tag: re.compile(rf'(<{tag}>)([\s\S]*?)()') for tag in ('thinking', 'think', 'tool_use', 'tool_result')} + _hist_pat = re.compile(r'<(history|key_info)>[\s\S]*?') def _trunc(text): + text = _hist_pat.sub(lambda m: f'<{m.group(1)}>[...]', text) for pat in _pats.values(): text = pat.sub(lambda m: m.group(1) + m.group(2)[:max_len] + '...' + m.group(3) if len(m.group(2)) > max_len else m.group(0), text) return text for i, msg in enumerate(messages): if i >= len(messages) - keep_recent: break - if 'prompt' in msg: msg['prompt'] = _trunc(msg['prompt']) - elif 'content' in msg and 'prompt' not in msg: - c = msg['content'] - if isinstance(c, str): msg['content'] = _trunc(c) - elif isinstance(c, list): - for block in c: - if isinstance(block, dict) and block.get('type') == 'text' and isinstance(block.get('text'), str): - block['text'] = _trunc(block['text']) + c = msg['content'] + if isinstance(c, str): msg['content'] = _trunc(c) + elif isinstance(c, list): + for block in c: + if isinstance(block, dict) and block.get('type') == 'text' and isinstance(block.get('text'), str): + block['text'] = _trunc(block['text']) print(f"[Cut] {_before} -> {sum(len(json.dumps(m, ensure_ascii=False)) for m in messages)}") return messages def _sanitize_leading_user_msg(msg): """把 user 消息里的 tool_result 块改写成纯文本,避免孤立引用。 history 统一使用 Claude content-block 格式:content 是 list of blocks。""" - msg = dict(msg) # 浅拷贝外层 dict;content 在 L56 整体替换而非原地修改,故原对象的 content 不受影响 + msg = dict(msg) # 浅拷贝外层 dict content = msg.get('content') if not isinstance(content, list): return msg texts = [] @@ -411,7 +410,7 @@ class BaseSession: self.api_key = cfg['apikey'] self.api_base = cfg['apibase'].rstrip('/') self.default_model = cfg.get('model', '') - self.context_win = cfg.get('context_win', 20000) + self.context_win = cfg.get('context_win', 24000) self.history = [] self.lock = threading.Lock() self.system = "" @@ -481,7 +480,7 @@ class LLMSession(BaseSession): class NativeClaudeSession(BaseSession): def __init__(self, cfg): super().__init__(cfg) - self.context_win = cfg.get("context_win", 24000) + self.context_win = cfg.get("context_win", 28000) self.no_system_prompt = cfg.get("no_system_prompt", False) def raw_ask(self, messages, tools=None, system=None, model=None, temperature=0.5, max_tokens=6144):