From 6bed654dce68c0d7b61e24960599bbff97bd9b44 Mon Sep 17 00:00:00 2001 From: Liang Jiaqing Date: Fri, 31 Jul 2026 21:56:27 +0800 Subject: [PATCH] feat: improve conductor files and runtime handling --- agentmain.py | 3 ++ frontends/conductor.html | 90 +++++++++++++++++++++++++++++++++++++--- frontends/conductor.py | 77 ++++++++++++++++++++++++++++++---- frontends/stapp.py | 59 ++++++++++++++------------ ga.py | 2 +- llmcore.py | 5 ++- 6 files changed, 195 insertions(+), 41 deletions(-) diff --git a/agentmain.py b/agentmain.py index 6c5746bd..6f04ccef 100644 --- a/agentmain.py +++ b/agentmain.py @@ -110,6 +110,9 @@ class GenericAgent: if not self.is_running: return print('Abort current task...') self.stop_sig = True + for sess in getattr(self.llmclient.backend, '_sessions', [self.llmclient.backend]): + try: sess.active_response.close() + except Exception: pass if self.handler is not None: self.handler.code_stop_signal.append(1) def put_task(self, query, source="user", images=None): diff --git a/frontends/conductor.html b/frontends/conductor.html index a8155f60..b3a19520 100644 --- a/frontends/conductor.html +++ b/frontends/conductor.html @@ -218,6 +218,7 @@ .composer { padding: 14px; border-top: 1px solid rgba(31,42,68,.075); display: flex; gap: 12px; align-items: end; background: rgba(255,255,255,.40); } + .composer.dragging { background: rgba(59,130,246,.13); outline: 2px dashed #3b82f6; outline-offset: -6px; } textarea#input { flex: 1; height: 52px; max-height: 140px; resize: none; outline: none; color: var(--text); background: rgba(255,255,255,.92); border: 1px solid rgba(31,42,68,.12); border-radius: 18px; padding: 14px 15px; font: inherit; line-height: 1.35; } @@ -225,6 +226,19 @@ button { height: 52px; min-width: 92px; border: 0; border-radius: 18px; color: white; font-weight: 750; cursor: pointer; background: linear-gradient(135deg, #3b82f6, #10b981); box-shadow: 0 10px 26px rgba(59,130,246,.22); } + .file-open, .attach { min-width: 0; padding: 0 10px; box-shadow: none; } + .file-open { height: 28px; border-radius: 9px; } + .attach { width: 44px; height: 52px; font-size: 18px; border-radius: 18px; } + .file-link { color: inherit; text-decoration: underline; cursor: pointer; } + #fileDialog { width: min(1100px, 96vw); height: min(800px, 92vh); padding: 0; border: 0; border-radius: 18px; box-shadow: 0 24px 80px #0004; } + #fileDialog::backdrop { background: #0f172a66; backdrop-filter: blur(3px); } + .file-bar { display: flex; align-items: center; gap: 8px; padding: 12px; border-bottom: 1px solid var(--border); } + .file-bar code { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .file-bar button { min-width: 0; height: 34px; padding: 0 12px; border-radius: 10px; box-shadow: none; } + #fileBody { height: calc(100% - 59px); overflow: auto; padding: 12px; } + .file-row { display: flex; gap: 10px; padding: 9px 11px; border-radius: 9px; cursor: pointer; } + .file-row:hover { background: #eef2ff; } .file-row span:first-child { flex: 1; } .file-size { color: var(--muted); } + .file-preview { line-height: 1.55; } .file-preview pre { white-space: pre-wrap; overflow-wrap: anywhere; } /* ---------- conductor 输出(横跨底部的独立卡片,默认收起) ---------- */ .console { @@ -314,18 +328,29 @@
Conversation
-
connecting...
+
· connecting...
-
- - +
+ + +
+ +
+
+
+ + 下载文件 + +
+
+ ', height=0) +# Fixed target shared by full-app rendering and the one-second stream fragment. +# The badge is position:fixed, so this placeholder does not affect its visual position. +stats_slot = st.empty() + _injected = st.session_state.pop('_inject_prompt', None) prompt = st.chat_input("any task?") or _injected if prompt: @@ -454,33 +476,16 @@ if prompt: if hasattr(agent, '_pet_req') and not prompt.startswith('/'): agent._pet_req('state=walk') with st.chat_message("user"): st.markdown(prompt) _start_main_task(prompt) - mount_main_stream() + mount_main_stream(stats_slot) elif st.session_state.get('display_queue') is not None: # No new prompt but a task is mid-flight (typically a /btw rerun) — resume drain. - mount_main_stream() + mount_main_stream(stats_slot) -# Usage/time label remains after completion; while running only its fragment updates once/sec. +# Usage/time label remains after completion. During a task render_main_stream refreshes it. _has_task_stats = 'task_start_ts' in st.session_state _is_running = st.session_state.get('display_queue') is not None -if _has_task_stats: - def _render_stat_badge(): - if not hasattr(llmcore, 'STATS'): return - end_ts = time.time() if _is_running else st.session_state.get('task_end_ts', time.time()) - secs = max(0, int(end_ts - st.session_state.task_start_ts)) - stats = dict(llmcore.STATS) - short = lambda n: f'{n / 1000:.0f}k' if n >= 1000 else str(n) - usage = ((f"{stats['session']} │ " if stats.get('session') else '') + - f"{short(stats['ctx'])} chars·{stats['msgs']}msgs │ " - f"in {short(stats.get('inp', 0))} toks·cached{short(stats.get('cached', 0))}·out{short(stats.get('out', 0))} │ " - if 'ctx' in stats else '') - st.markdown(f'
{usage}{secs // 60}:{secs % 60:02d}
', - unsafe_allow_html=True) - if _is_running: - @st.fragment(run_every=timedelta(seconds=1)) - def _live_stat_badge(): _render_stat_badge() - _live_stat_badge() - else: - _render_stat_badge() +if _has_task_stats and not _is_running: + _render_stat_badge(is_running=False, target=stats_slot) if _has_task_stats or _is_running: st.markdown( diff --git a/ga.py b/ga.py index 8ba737d1..c56db03f 100644 --- a/ga.py +++ b/ga.py @@ -312,7 +312,7 @@ class GenericAgentHandler(BaseHandler): cwd = os.path.normpath(os.path.abspath(raw_path)) code_cwd = os.path.normpath(self.cwd) maxlen = self._get_tool_maxlen(10000, args) - if timeout > 600: result = '[ERROR] Timeout must be <= 600 seconds, run time-consuming code in background subprocess and monitor it!' + if timeout > 600: result = '[ERROR] Timeout must be <= 600 seconds; code not executed. Run time-consuming code in the background instead of waiting for it to finish in the foreground, verify it started successfully, and monitor it until completion or failure.' elif code_type == 'python' and _arg(args, "inline_eval", False, bool): ns = {'handler':self, 'parent':self.parent, 'history':json.dumps(self.parent.llmclient.backend.history)} old_cwd = os.getcwd() diff --git a/llmcore.py b/llmcore.py index de3d38b5..7befa74b 100644 --- a/llmcore.py +++ b/llmcore.py @@ -174,7 +174,9 @@ def _parse_claude_sse(resp_lines): if current_block and current_block.get("type") == "text": current_block["text"] += text if text: yield text elif delta.get("type") == "thinking_delta": - if current_block and current_block.get("type") == "thinking": current_block["thinking"] += delta.get("thinking", "") + thinking = delta.get("thinking", "") + if current_block and current_block.get("type") == "thinking": current_block["thinking"] += thinking + if thinking: yield thinking elif delta.get("type") == "signature_delta": if current_block and current_block.get("type") == "thinking": current_block["signature"] = current_block.get("signature", "") + delta.get("signature", "") @@ -396,6 +398,7 @@ def _stream_with_retry(sess, url, headers, payload, parse_fn): try: with requests.post(url, headers=headers, json=payload, stream=sess.stream, timeout=(sess.connect_timeout, sess.read_timeout), proxies=sess.proxies, verify=sess.verify) as r: + sess.active_response = r if r.status_code >= 400: #pathlib.Path(__file__).parent.joinpath('temp','bad_requests.json').write_text(json.dumps({"url":url,"headers":headers,"payload":payload,"t":time.time()},ensure_ascii=False),encoding='utf-8') d = _delay(r, attempt) if r.status_code in _RETRYABLE and attempt < sess.max_retries else None