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.read_timeout = max(self.llmclient.backend.read_timeout, 1200)
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:
full_resp = ""; last_pos = 0; curr_turn = 0; turn_resps = []
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 time, json, re, threading, queue
from datetime import timedelta
from agentmain import GeneraticAgent
import chatapp_common # activate /continue command (monkey patches GeneraticAgent)
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 = {
'zh': {
'force_stop': '强行停止任务',
'reinject_tools': '重新注入工具',
'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': {
'force_stop': 'Force Stop',
'reinject_tools': 'Reinject Tools',
'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)
@@ -72,14 +87,6 @@ def render_sidebar():
agent.next_llm(selected_idx); st.rerun(scope="fragment")
if st.button(T('force_stop')):
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')):
kwargs = {'creationflags': 0x08} if sys.platform == 'win32' else {}
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
st.toast("Desktop pet started")
if LANG == 'zh':
if st.button('🎯 给我找点事做'):
st.session_state['_inject_prompt'] = '按照自主行动的规划部分,充分分析我的情况,给我生成一批TODO,务必让我感兴趣'
st.rerun(scope="app")
st.divider()
if st.button("开始空闲自主行动"):
st.session_state.last_reply_time = int(time.time()) - 1800
if st.button(T('suggest_btn')):
st.session_state['_inject_prompt'] = T('suggest_prompt')
st.rerun(scope="app")
st.divider()
if st.session_state.get('loop_enabled'):
if st.button("⏹️ Stop Loop"):
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.toast("已将上次回复时间设为1800秒前,自主行动已激活"); st.rerun(scope="app")
if st.session_state.autonomous_enabled:
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("🔴 自主行动已停止")
st.toast("✅"); st.rerun(scope="app")
st.caption(T('auto_off_cap'))
with st.sidebar: render_sidebar()
def fold_turns(text):
@@ -226,6 +245,10 @@ def render_main_stream(prompt=None):
if response:
st.session_state.messages.append({"role": "assistant", "content": response})
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 = []
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.
render_main_stream()
if st.session_state.autonomous_enabled:
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)
# ── 空闲自主行动:fragment 定时检测,替代 launch.pyw 的 idle_monitor ──
@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}')
_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,不允许继续重试。"
elif turn % 7 == 0:
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:
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')
injprompt = consume_file(self.parent.task_dir, '_intervene')
+7 -41
View File
@@ -22,31 +22,6 @@ def start_streamlit(port):
proc = subprocess.Popen(cmd)
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;
document.addEventListener('paste', e => {
const items = e.clipboardData?.items; if (!items) return;
@@ -65,21 +40,13 @@ PASTE_HOOK_JS = """if (!window._pasteHooked) { window._pasteHooked = true;
}, true);
}"""
def idle_monitor():
last_trigger_time = 0
def paste_hook_injector():
"""注入剪贴板粘贴图片钩子,等窗口加载后执行一次,之后每60秒补注入防页面刷新丢失"""
time.sleep(5) # 等 webview 加载
while True:
time.sleep(5)
try:
window.evaluate_js(PASTE_HOOK_JS)
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}')
try: window.evaluate_js(PASTE_HOOK_JS)
except: pass
time.sleep(60)
if __name__ == '__main__':
import argparse
@@ -140,8 +107,7 @@ if __name__ == '__main__':
print('[Launch] Task Scheduler started (duplicate prevented by scheduler port lock)')
else: print('[Launch] Task Scheduler not enabled (--sched)')
monitor_thread = threading.Thread(target=idle_monitor, daemon=True)
monitor_thread.start()
threading.Thread(target=paste_hook_injector, daemon=True).start()
if os.name == 'nt':
screen_width = get_screen_width()
x_pos = screen_width - WINDOW_WIDTH - RIGHT_PADDING