feat: loop mode + idle auto-action in stapp, cleanup launch.pyw

- stapp.py: add loop mode (auto-inject prompt after each reply)
- stapp.py: replace DOM-based idle monitor with st.fragment timer
- stapp.py: i18n for all new UI text and LLM prompts
- launch.pyw: remove inject/get_last_reply_time/idle_monitor, keep only paste hook
- agentmain/ga: raise max_turns 80->180, DANGER thresholds accordingly
- remove obsolete tool_usable_history.json
This commit is contained in:
Liang Jiaqing committed 2026-06-19 19:00:44 +08:00
1 parent 0ca5811e24
commit df025ab34f
5 files changed
+73 -75

No files matched your search

+1 -1
View File
@@ -162,7 +162,7 @@ class GenericAgent:
self.llmclient.backend.stream = False self.llmclient.backend.stream = False
self.llmclient.backend.read_timeout = max(self.llmclient.backend.read_timeout, 1200) self.llmclient.backend.read_timeout = max(self.llmclient.backend.read_timeout, 1200)
gen = agent_runner_loop(self.llmclient, sys_prompt, raw_query, handler, TOOLS_SCHEMA, gen = agent_runner_loop(self.llmclient, sys_prompt, raw_query, handler, TOOLS_SCHEMA,
max_turns=80, verbose=self.verbose, yield_info=True) max_turns=180, verbose=self.verbose, yield_info=True)
try: try:
full_resp = ""; last_pos = 0; curr_turn = 0; turn_resps = [] full_resp = ""; last_pos = 0; curr_turn = 0; turn_resps = []
for chunk in gen: for chunk in gen:
-1
View File
@@ -1 +0,0 @@
[{"role":"user","content":[{"type":"text","text":"code_run个helloworld,根据结果简单评价你的工具配置"}]},{"role":"assistant","content":[{"type":"text","text":"\n\n<summary>用户要求code_run helloworld并评价工具配置</summary>\n\n```python\nprint(\"Hello, World!\")\n```"},{"type":"tool_use","id":"tooluse_GlmTNw190WGgK4sBBOiDHK","name":"code_run","input":{"script":"print(\"Hello, World!\")"}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"tooluse_GlmTNw190WGgK4sBBOiDHK","content":"{\"status\": \"success\", \"stdout\": \"Hello, World!\\r\\n\", \"exit_code\": 0}"},{"type":"text","text":"\n### [WORKING MEMORY]\n<history>\n\n[USER]: code_run个helloworld,根据结果简单评价你的工具配置\n</history>\nCurrent turn: 1\n"}]},{"role":"assistant","content":[{"type":"text","text":"\n\n<summary>code_run helloworld成功,评价工具配置</summary>\n\n工具配置没问题。`code_run` 返回了结构化的 JSON(`status`、`stdout`、`exit_code`),执行链路通畅,cwd 默认落在 temp 目录。\n\n刚才还验证了 `inline_eval` 能正确访问 agent 内部状态并导出 history,`file_patch` 也能精准改文件。整套工具链从代码执行、文件读写、到 agent 内省都是通的。"}]}]
+63 -30
View File
@@ -13,6 +13,7 @@ sys.path.append(os.path.abspath(script_dir))
import streamlit as st import streamlit as st
import time, json, re, threading, queue import time, json, re, threading, queue
from datetime import timedelta
from agentmain import GeneraticAgent from agentmain import GeneraticAgent
import chatapp_common # activate /continue command (monkey patches GeneraticAgent) import chatapp_common # activate /continue command (monkey patches GeneraticAgent)
from continue_cmd import handle_frontend_command, reset_conversation, list_sessions, extract_ui_messages from continue_cmd import handle_frontend_command, reset_conversation, list_sessions, extract_ui_messages
@@ -34,13 +35,27 @@ if LANG not in ('zh', 'en'): LANG = 'zh'
I18N = { I18N = {
'zh': { 'zh': {
'force_stop': '强行停止任务', 'force_stop': '强行停止任务',
'reinject_tools': '重新注入工具',
'desktop_pet': '🐱 桌面宠物', 'desktop_pet': '🐱 桌面宠物',
'suggest_btn': '🎯 给我找点事做',
'suggest_prompt': '按照自主行动的规划部分,充分分析我的情况,给我生成一批TODO,务必让我感兴趣',
'auto_start': '开始空闲自主行动',
'auto_pause': '⏸️ 禁止自主行动',
'auto_enable': '▶️ 允许自主行动',
'auto_on_cap': '🟢 自主行动运行中,会在你离开它30分钟后自动进行',
'auto_off_cap': '🔴 自主行动已停止',
'auto_prompt': '[AUTO]🤖 用户已经离开超过30分钟,作为自主智能体,请阅读自动化sop,执行自动任务。',
}, },
'en': { 'en': {
'force_stop': 'Force Stop', 'force_stop': 'Force Stop',
'reinject_tools': 'Reinject Tools',
'desktop_pet': '🐱 Desktop Pet', 'desktop_pet': '🐱 Desktop Pet',
'suggest_btn': '🎯 Suggest tasks',
'suggest_prompt': 'Following the planning section of autonomous sop, analyze my situation thoroughly and generate a batch of TODOs that will interest me.',
'auto_start': 'Start idle auto-action',
'auto_pause': '⏸️ Pause auto-action',
'auto_enable': '▶️ Enable auto-action',
'auto_on_cap': '🟢 Auto-action enabled, triggers after 30min idle',
'auto_off_cap': '🔴 Auto-action disabled',
'auto_prompt': '[AUTO]🤖 User has been idle for over 30 minutes. As an autonomous agent, read the automation SOP and execute automatic tasks.',
}, },
} }
def T(key): return I18N.get(LANG, I18N['zh']).get(key, key) def T(key): return I18N.get(LANG, I18N['zh']).get(key, key)
@@ -72,14 +87,6 @@ def render_sidebar():
agent.next_llm(selected_idx); st.rerun(scope="fragment") agent.next_llm(selected_idx); st.rerun(scope="fragment")
if st.button(T('force_stop')): if st.button(T('force_stop')):
agent.abort(); st.toast("Stop signal sended"); st.rerun() agent.abort(); st.toast("Stop signal sended"); st.rerun()
if st.button(T('reinject_tools')):
agent.llmclient.last_tools = ''
try:
hist_path = os.path.join(script_dir, '..', 'assets', 'tool_usable_history.json')
with open(hist_path, 'r', encoding='utf-8') as f: tool_hist = json.load(f)
agent.llmclient.backend.history.extend(tool_hist)
st.toast(f"Tools injected")
except Exception as e: st.toast(f"Injected tools failed: {e}")
if st.button(T('desktop_pet')): if st.button(T('desktop_pet')):
kwargs = {'creationflags': 0x08} if sys.platform == 'win32' else {} kwargs = {'creationflags': 0x08} if sys.platform == 'win32' else {}
pet_script = os.path.join(script_dir, 'desktop_pet_v2.pyw') pet_script = os.path.join(script_dir, 'desktop_pet_v2.pyw')
@@ -103,25 +110,37 @@ def render_sidebar():
agent._turn_end_hooks['pet'] = _pet_hook agent._turn_end_hooks['pet'] = _pet_hook
st.toast("Desktop pet started") st.toast("Desktop pet started")
if LANG == 'zh': if st.button(T('suggest_btn')):
if st.button('🎯 给我找点事做'): st.session_state['_inject_prompt'] = T('suggest_prompt')
st.session_state['_inject_prompt'] = '按照自主行动的规划部分,充分分析我的情况,给我生成一批TODO,务必让我感兴趣' st.rerun(scope="app")
st.rerun(scope="app") st.divider()
st.divider() if st.session_state.get('loop_enabled'):
if st.button("开始空闲自主行动"): if st.button("⏹️ Stop Loop"):
st.session_state.last_reply_time = int(time.time()) - 1800 st.session_state.loop_enabled = False
st.toast("⏹️ Loop stopped"); st.rerun(scope="app")
st.caption("🔁 Looping")
else:
loop_prompt = st.text_input("Loop prompt", value="继续" if LANG=='zh' else 'next', key="loop_prompt_input")
if st.button("🔁 Loop!"):
st.session_state.loop_enabled = True
st.session_state.loop_prompt = loop_prompt
st.session_state['_inject_prompt'] = loop_prompt
st.toast("🔁 Looping"); st.rerun(scope="app")
st.divider()
if st.button(T('auto_start')):
st.session_state.last_reply_time = int(time.time()) - 1800
st.session_state.autonomous_enabled = True
st.rerun(scope="app")
if st.session_state.autonomous_enabled:
if st.button(T('auto_pause')):
st.session_state.autonomous_enabled = False
st.toast(T('auto_pause')); st.rerun(scope="app")
st.caption(T('auto_on_cap'))
else:
if st.button(T('auto_enable'), type="primary"):
st.session_state.autonomous_enabled = True st.session_state.autonomous_enabled = True
st.toast("已将上次回复时间设为1800秒前,自主行动已激活"); st.rerun(scope="app") st.toast("✅"); st.rerun(scope="app")
if st.session_state.autonomous_enabled: st.caption(T('auto_off_cap'))
if st.button("⏸️ 禁止自主行动"):
st.session_state.autonomous_enabled = False
st.toast("⏸️ 已禁止自主行动"); st.rerun(scope="app")
st.caption("🟢 自主行动运行中,会在你离开它30分钟后自动进行")
else:
if st.button("▶️ 允许自主行动", type="primary"):
st.session_state.autonomous_enabled = True
st.toast("✅ 已允许自主行动"); st.rerun(scope="app")
st.caption("🔴 自主行动已停止")
with st.sidebar: render_sidebar() with st.sidebar: render_sidebar()
def fold_turns(text): def fold_turns(text):
@@ -226,6 +245,10 @@ def render_main_stream(prompt=None):
if response: if response:
st.session_state.messages.append({"role": "assistant", "content": response}) st.session_state.messages.append({"role": "assistant", "content": response})
st.session_state.last_reply_time = int(time.time()) st.session_state.last_reply_time = int(time.time())
# ── 循环回调:回答完成后自动注入下一条 ──
if st.session_state.get('loop_enabled'):
st.session_state['_inject_prompt'] = st.session_state.get('loop_prompt', '继续')
st.rerun()
if "messages" not in st.session_state: st.session_state.messages = [] if "messages" not in st.session_state: st.session_state.messages = []
for msg in st.session_state.messages: for msg in st.session_state.messages:
@@ -334,5 +357,15 @@ 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. # No new prompt but a task is mid-flight (typically a /btw rerun) — resume drain.
render_main_stream() render_main_stream()
if st.session_state.autonomous_enabled: # ── 空闲自主行动:fragment 定时检测,替代 launch.pyw 的 idle_monitor ──
st.markdown(f"""<div id="last-reply-time" style="display:none">{st.session_state.get('last_reply_time', int(time.time()))}</div>""", unsafe_allow_html=True) @st.fragment(run_every=timedelta(minutes=5))
def _idle_checker():
if not st.session_state.get('autonomous_enabled'): return
if st.session_state.get('display_queue') is not None: return # 正在运行中
if st.session_state.get('loop_enabled'): return # 循环模式自己管
last = st.session_state.get('last_reply_time', int(time.time()))
if time.time() - last > 1800:
st.session_state['_inject_prompt'] = T('auto_prompt')
st.session_state['last_reply_time'] = int(time.time()) # 防重入
st.rerun(scope="app")
_idle_checker()
+2 -2
View File
@@ -561,7 +561,7 @@ class GenericAgentHandler(BaseHandler):
self.history_info.append(f'[Agent] {summary}') self.history_info.append(f'[Agent] {summary}')
_plan = self._in_plan_mode() _plan = self._in_plan_mode()
if turn % 75 == 0 and (not _plan): if turn % 175 == 0 and (not _plan):
next_prompt += f"\n\n[DANGER] 已连续执行第 {turn} 轮。必须总结情况进行ask_user,不允许继续重试。" next_prompt += f"\n\n[DANGER] 已连续执行第 {turn} 轮。必须总结情况进行ask_user,不允许继续重试。"
elif turn % 7 == 0: elif turn % 7 == 0:
next_prompt += f"\n\n[SYSTEM] 已连续执行第 {turn} 轮。建议调用update_working_checkpoint保存关键上下文。禁止无效重试;若无有效进展,必须切换策略:1. 探测物理边界 2. **重读相关SOP**" next_prompt += f"\n\n[SYSTEM] 已连续执行第 {turn} 轮。建议调用update_working_checkpoint保存关键上下文。禁止无效重试;若无有效进展,必须切换策略:1. 探测物理边界 2. **重读相关SOP**"
@@ -569,7 +569,7 @@ class GenericAgentHandler(BaseHandler):
if _plan and turn >= 10 and turn % 5 == 0: if _plan and turn >= 10 and turn % 5 == 0:
next_prompt = f"[Plan Hint] 正在计划模式。必须 file_read({_plan}) 确认当前步骤,回复开头引用:📌 当前步骤:...\n\n" + next_prompt next_prompt = f"[Plan Hint] 正在计划模式。必须 file_read({_plan}) 确认当前步骤,回复开头引用:📌 当前步骤:...\n\n" + next_prompt
if _plan and turn >= 120: next_prompt += f"\n\n[DANGER] Plan模式已运行 {turn} 轮,已达上限。必须 ask_user 汇报进度并确认是否继续。" if _plan and turn >= 190: next_prompt += f"\n\n[DANGER] Plan模式已运行 {turn} 轮,已达上限。必须 ask_user 汇报进度并确认是否继续。"
injkeyinfo = consume_file(self.parent.task_dir, '_keyinfo') injkeyinfo = consume_file(self.parent.task_dir, '_keyinfo')
injprompt = consume_file(self.parent.task_dir, '_intervene') injprompt = consume_file(self.parent.task_dir, '_intervene')
+7 -41
View File
@@ -22,31 +22,6 @@ def start_streamlit(port):
proc = subprocess.Popen(cmd) proc = subprocess.Popen(cmd)
atexit.register(proc.kill) atexit.register(proc.kill)
def inject(text):
window.evaluate_js(f"""
const textarea = document.querySelector('textarea[data-testid="stChatInputTextArea"]');
if (textarea) {{
// 1. 用原生 setter 设置值(绕过 React)
const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
nativeTextAreaValueSetter.call(textarea, {repr(text)});
// 2. 触发 React 的 input 事件
textarea.dispatchEvent(new Event('input', {{ bubbles: true }}));
// 3. 触发 change 事件(有些组件需要)
textarea.dispatchEvent(new Event('change', {{ bubbles: true }}));
// 4. 延迟提交
setTimeout(() => {{
const btn = document.querySelector('[data-testid="stChatInputSubmitButton"]');
if (btn) {{btn.click();console.log('Submitted:', {repr(text)});}}
}}, 200);
}}""")
def get_last_reply_time():
last = window.evaluate_js("""
const el = document.getElementById('last-reply-time');
el ? parseInt(el.textContent) : 0;
""") or 0
return last or int(time.time())
PASTE_HOOK_JS = """if (!window._pasteHooked) { window._pasteHooked = true; PASTE_HOOK_JS = """if (!window._pasteHooked) { window._pasteHooked = true;
document.addEventListener('paste', e => { document.addEventListener('paste', e => {
const items = e.clipboardData?.items; if (!items) return; const items = e.clipboardData?.items; if (!items) return;
@@ -65,21 +40,13 @@ PASTE_HOOK_JS = """if (!window._pasteHooked) { window._pasteHooked = true;
}, true); }, true);
}""" }"""
def idle_monitor(): def paste_hook_injector():
last_trigger_time = 0 """注入剪贴板粘贴图片钩子,等窗口加载后执行一次,之后每60秒补注入防页面刷新丢失"""
time.sleep(5) # 等 webview 加载
while True: while True:
time.sleep(5) try: window.evaluate_js(PASTE_HOOK_JS)
try: except: pass
window.evaluate_js(PASTE_HOOK_JS) time.sleep(60)
now = time.time()
if now - last_trigger_time < 120: continue
last_reply = get_last_reply_time()
if now - last_reply > 1800:
print('[Idle Monitor] Detected idle state, injecting task...')
inject("[AUTO]🤖 用户已经离开超过30分钟,作为自主智能体,请阅读自动化sop,执行自动任务。")
last_trigger_time = now
except Exception as e:
print(f'[Idle Monitor] Error: {e}')
if __name__ == '__main__': if __name__ == '__main__':
import argparse import argparse
@@ -140,8 +107,7 @@ if __name__ == '__main__':
print('[Launch] Task Scheduler started (duplicate prevented by scheduler port lock)') print('[Launch] Task Scheduler started (duplicate prevented by scheduler port lock)')
else: print('[Launch] Task Scheduler not enabled (--sched)') else: print('[Launch] Task Scheduler not enabled (--sched)')
monitor_thread = threading.Thread(target=idle_monitor, daemon=True) threading.Thread(target=paste_hook_injector, daemon=True).start()
monitor_thread.start()
if os.name == 'nt': if os.name == 'nt':
screen_width = get_screen_width() screen_width = get_screen_width()
x_pos = screen_width - WINDOW_WIDTH - RIGHT_PADDING x_pos = screen_width - WINDOW_WIDTH - RIGHT_PADDING