myagent-ai 1.23.37 → 1.23.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -215,6 +215,8 @@ class MainAgent(BaseAgent):
215
215
  self._exec_event_counter: int = 0
216
216
  # 活跃会话上下文追踪(用于消息注入)
217
217
  self.active_contexts: Dict[str, AgentContext] = {}
218
+ # [v1.23.37] 会话取消标志(由 api_server.handle_chat_stop 设置)
219
+ self._cancelled_sessions: set = set()
218
220
 
219
221
  def init_context_builder(self, memory_manager=None, skill_registry=None, knowledge_base_dir=None, context_window=None):
220
222
  """初始化 Context Builder(在系统启动后调用,注入依赖)"""
@@ -737,6 +739,14 @@ class MainAgent(BaseAgent):
737
739
  logger.info(f"[{task_id}] V2 迭代 {self._iteration_count}/{max_iter}")
738
740
  _emitted_reasoning_this_iter = False # [v1.15.8] 本轮是否已发送过 v2_reasoning 事件(防 TTS 重复)
739
741
 
742
+ # [v1.23.37] 检查是否收到停止信号
743
+ if context.session_id in self._cancelled_sessions:
744
+ logger.info(f"[{task_id}] 收到停止信号,终止执行循环")
745
+ context.working_memory["final_response"] = "⏹️ 任务已被用户停止"
746
+ await self._emit_v2_event("v2_stopped", {"reason": "用户手动停止"}, stream_callback)
747
+ self._cancelled_sessions.discard(context.session_id)
748
+ break
749
+
740
750
  # ── 检查配置热加载广播 ──
741
751
  if self.config_broadcaster:
742
752
  reloaded, reload_type = await self.config_broadcaster.check_and_wait(task_id)
@@ -221,6 +221,8 @@ class ExecutionEngine:
221
221
  self.work_dir = work_dir or os.getcwd()
222
222
  self._permission_checker = permission_checker # callable(perm_name) -> bool
223
223
  self._agent_name = agent_name
224
+ # [v1.23.37] 当前正在运行的子进程引用,用于 cancel_current() 终止
225
+ self._current_process: Optional[asyncio.subprocess.Process] = None
224
226
 
225
227
  # 安全: 合并正则模式 + 字符串黑名单
226
228
  self._blocked = set(self.DANGEROUS_COMMANDS)
@@ -606,6 +608,7 @@ class ExecutionEngine:
606
608
  cwd=work_dir,
607
609
  env=self._get_env(env),
608
610
  )
611
+ self._current_process = process # [v1.23.37] 保存引用
609
612
 
610
613
  try:
611
614
  # 使用流式读取代替 communicate(),确保超时后能保留已产生的输出
@@ -747,6 +750,7 @@ class ExecutionEngine:
747
750
  cwd=work_dir,
748
751
  env=self._get_env(env),
749
752
  )
753
+ self._current_process = process # [v1.23.37] 保存引用
750
754
 
751
755
  # 使用流式读取代替 communicate(),确保超时后能保留已产生的输出
752
756
  stdout_chunks = []
@@ -948,6 +952,7 @@ class ExecutionEngine:
948
952
  cwd=work_dir,
949
953
  env=self._get_env(env),
950
954
  )
955
+ self._current_process = process # [v1.23.37] 保存引用
951
956
  try:
952
957
  stdout, stderr = await asyncio.wait_for(
953
958
  process.communicate(), timeout=timeout
@@ -989,6 +994,26 @@ class ExecutionEngine:
989
994
  metadata={"mode": "sandbox"},
990
995
  )
991
996
 
997
+ # [v1.23.37] ── 取消当前执行 ──
998
+
999
+ def cancel_current(self):
1000
+ """终止当前正在运行的子进程(由 /api/chat/stop 调用)"""
1001
+ proc = self._current_process
1002
+ if proc is None:
1003
+ return False
1004
+ try:
1005
+ if proc.returncode is None: # 进程仍在运行
1006
+ proc.kill()
1007
+ logger.info(f"[executor] 已终止子进程 PID={proc.pid}")
1008
+ self._current_process = None
1009
+ return True
1010
+ except ProcessLookupError:
1011
+ pass # 进程已结束
1012
+ except Exception as e:
1013
+ logger.debug(f"[executor] 终止子进程失败: {e}")
1014
+ self._current_process = None
1015
+ return False
1016
+
992
1017
  def set_execution_mode(self, mode: str) -> bool:
993
1018
  """切换执行模式。返回是否切换成功。"""
994
1019
  if mode not in ("local", "sandbox"):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.23.37",
3
+ "version": "1.23.38",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
package/web/api_server.py CHANGED
@@ -406,6 +406,8 @@ class ApiServer:
406
406
  r.add_post("/api/chat", self.handle_chat)
407
407
  r.add_post("/api/chat/stream", self.handle_chat_stream)
408
408
  r.add_post("/api/chat/inject", self.handle_chat_inject)
409
+ # [v1.23.37] 停止执行
410
+ r.add_post("/api/chat/stop", self.handle_chat_stop)
409
411
  r.add_post("/api/voice-optimize", self.handle_voice_optimize)
410
412
  r.add_post("/api/voice-stt", self.handle_voice_stt)
411
413
  r.add_get("/chat", self.handle_chat_page)
@@ -1911,6 +1913,10 @@ window.addEventListener('beforeunload', function() {{
1911
1913
  # {session_id: {running: bool, started_at: float, result: str, done: bool, error: str}}
1912
1914
  _running_sessions: Dict[str, Dict] = {}
1913
1915
  _running_sessions_ttl = 300 # 过期时间(秒),用于自动清理已完成会话
1916
+ # [v1.23.37] 存储后台 asyncio.Task 引用,用于取消执行
1917
+ _running_tasks: Dict[str, asyncio.Task] = {}
1918
+ # [v1.23.37] 会话取消标志,供 agent 执行循环检查
1919
+ _session_cancelled: Dict[str, bool] = {}
1914
1920
 
1915
1921
  async def _get_session_lock(self, session_id: str) -> asyncio.Lock:
1916
1922
  """获取或创建会话级锁(每个 session_id 一个锁,防并发覆盖 MainAgent 共享状态)"""
@@ -2210,10 +2216,15 @@ window.addEventListener('beforeunload', function() {{
2210
2216
  session_lock.release()
2211
2217
  except RuntimeError:
2212
2218
  pass
2219
+ # [v1.23.37] 清理任务引用和取消标志
2220
+ self._running_tasks.pop(session_id, None)
2221
+ self._session_cancelled.pop(session_id, None)
2213
2222
 
2214
2223
  # ── 启动后台任务(不等它完成,前端断开也不影响) ──
2215
2224
  logger.info(f"[{session_id}] 准备创建后台任务")
2216
2225
  bg_task = asyncio.create_task(_run_stream_task())
2226
+ self._running_tasks[session_id] = bg_task # [v1.23.37] 保存引用用于取消
2227
+ self._session_cancelled[session_id] = False # [v1.23.37] 初始化取消标志
2217
2228
  logger.info(f"[{session_id}] 后台任务已创建: {bg_task}")
2218
2229
 
2219
2230
  # ── SSE 事件循环:实时转发后台任务的事件到客户端 ──
@@ -2240,6 +2251,56 @@ window.addEventListener('beforeunload', function() {{
2240
2251
 
2241
2252
  return response
2242
2253
 
2254
+ # [v1.23.37] ── 停止执行 ──
2255
+
2256
+ async def handle_chat_stop(self, request):
2257
+ """POST /api/chat/stop - 停止正在执行的任务
2258
+
2259
+ 停止链:前端 abort → 调用此 API → 设置取消标志 + cancel task
2260
+ → agent 循环检测到取消标志 → 退出循环 → 任务结束
2261
+ """
2262
+ try:
2263
+ data = await request.json()
2264
+ except Exception:
2265
+ return web.json_response({"error": "invalid JSON"}, status=400)
2266
+
2267
+ session_id = data.get("session_id", "")
2268
+ if not session_id:
2269
+ return web.json_response({"ok": False, "error": "missing session_id"})
2270
+
2271
+ logger.info(f"[{session_id}] 收到停止执行请求")
2272
+
2273
+ # 1. 设置取消标志(agent 循环会检查)
2274
+ self._session_cancelled[session_id] = True
2275
+
2276
+ # 1.5 设置 MainAgent 上的取消标志(agent 循环会检查 _cancelled_sessions)
2277
+ if self.core.main_agent:
2278
+ self.core.main_agent._cancelled_sessions.add(session_id)
2279
+
2280
+ # 2. 尝试取消 asyncio.Task
2281
+ task = self._running_tasks.get(session_id)
2282
+ cancelled = False
2283
+ if task and not task.done():
2284
+ task.cancel()
2285
+ cancelled = True
2286
+ logger.info(f"[{session_id}] 已发送 task.cancel()")
2287
+
2288
+ # 3. 更新会话状态
2289
+ sess = self._running_sessions.get(session_id)
2290
+ if sess and sess.get("running"):
2291
+ sess["running"] = False
2292
+ sess["done"] = True
2293
+ sess["cancelled"] = True
2294
+
2295
+ # 4. 尝试终止正在运行的子进程(如果 executor 正在执行命令)
2296
+ try:
2297
+ if hasattr(self.core, 'executor') and self.core.executor:
2298
+ self.core.executor.cancel_current()
2299
+ except Exception as e:
2300
+ logger.debug(f"[{session_id}] executor 取消失败: {e}")
2301
+
2302
+ return web.json_response({"ok": True, "cancelled": cancelled})
2303
+
2243
2304
  async def handle_chat_inject(self, request):
2244
2305
  """POST /api/chat/inject - 注入消息到正在执行的任务,或进入排队序列"""
2245
2306
  try:
@@ -2392,6 +2392,16 @@ async function sendMessage(opts) {
2392
2392
 
2393
2393
  // ── Stop Generation ──
2394
2394
  function stopGenerating() {
2395
+ // [v1.23.37] 告知后端停止执行(kill subprocess + 设置取消标志)
2396
+ var sid = state.activeSessionId || '';
2397
+ if (sid) {
2398
+ fetch('/api/chat/stop', {
2399
+ method: 'POST',
2400
+ headers: { 'Content-Type': 'application/json' },
2401
+ body: JSON.stringify({ session_id: sid }),
2402
+ }).catch(function() {});
2403
+ }
2404
+ // 中断前端 SSE 连接
2395
2405
  if (state.abortController) {
2396
2406
  state.abortController.abort();
2397
2407
  }