myagent-ai 1.23.38 → 1.23.40

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.
package/agents/base.py CHANGED
@@ -371,6 +371,9 @@ class BaseAgent(ABC):
371
371
  model=request_kwargs.get("model", self.llm.model),
372
372
  reasoning=full_reasoning,
373
373
  )
374
+ except asyncio.CancelledError:
375
+ # [v1.23.38] 传播取消信号,不吞掉
376
+ raise
374
377
  except Exception as e:
375
378
  logger.error(f"LLM 流式调用失败: {e}")
376
379
  return LLMResponse(success=False, error=str(e))
@@ -1569,6 +1569,14 @@ class MainAgent(BaseAgent):
1569
1569
 
1570
1570
  all_tool_outputs = "\n".join(tool_outputs_parts)
1571
1571
 
1572
+ # [v1.23.38] 工具执行完毕后检查取消信号
1573
+ if context.session_id in self._cancelled_sessions:
1574
+ logger.info(f"[{task_id}] 收到停止信号,跳过 LLM 回调")
1575
+ context.working_memory["final_response"] = "⏹️ 任务已被用户停止"
1576
+ await self._emit_v2_event("v2_stopped", {"reason": "用户手动停止"}, stream_callback)
1577
+ self._cancelled_sessions.discard(context.session_id)
1578
+ break
1579
+
1572
1580
  # Step 12: 有工具执行完毕 → 统一回调 LLM(不再根据 finish 标签判断)
1573
1581
  # 所有工具结果已收集到 all_tool_outputs(或任一超时提前收集),直接喂回 LLM 继续执行
1574
1582
  _timeout_info = "(有工具超时,已停止剩余工具)" if _has_timeout else f"({len(tool_outputs_parts)} 个工具结果已收集)"
@@ -1606,9 +1614,9 @@ class MainAgent(BaseAgent):
1606
1614
  _tp.initialize()
1607
1615
  _tp.save_task(
1608
1616
  task_id=task_id,
1609
- description=context.user_input or "未完成任务",
1617
+ description=context.user_message or "未完成任务",
1610
1618
  session_id=context.session_id or "",
1611
- agent_path=context.agent_path or "",
1619
+ agent_path=context.metadata.get("agent_path", "") or "",
1612
1620
  status="pending",
1613
1621
  metadata={"iterations": self._iteration_count, "reason": "达到最大迭代次数"},
1614
1622
  last_message=f"达到最大迭代次数 {max_iter},共执行 {self._iteration_count} 次",
@@ -1662,8 +1670,12 @@ class MainAgent(BaseAgent):
1662
1670
  """[v1.22.0] V2 工具执行 — 统一分发到 ToolDispatcher"""
1663
1671
  try:
1664
1672
  import json as _json
1673
+ import html as _html
1665
1674
  try:
1666
- params = _json.loads(parms_str) if parms_str else {}
1675
+ # [v1.23.38] 防御性修复: 解码 HTML 实体 (& & 等)
1676
+ # LLM 可能在 XML 输出中使用 & 代替 &,导致 bash 命令失败
1677
+ _clean_parms = _html.unescape(parms_str) if parms_str else ""
1678
+ params = _json.loads(_clean_parms) if _clean_parms else {}
1667
1679
  except (_json.JSONDecodeError, TypeError):
1668
1680
  params = {"raw_input": parms_str}
1669
1681
 
@@ -48,6 +48,7 @@ Fault-tolerance features:
48
48
 
49
49
  from __future__ import annotations
50
50
 
51
+ import html
51
52
  import re
52
53
  from dataclasses import dataclass, field
53
54
  from typing import Any, Dict, List
@@ -227,7 +228,7 @@ def _extract_tag_content(text: str, tag_name: str, stop_tags: List[str] | None =
227
228
  re.DOTALL | re.IGNORECASE,
228
229
  )
229
230
  if m:
230
- return m.group(1)
231
+ return html.unescape(m.group(1))
231
232
 
232
233
  # Conservative mode: only extract properly closed tags, skip all fallbacks
233
234
  if conservative:
@@ -251,7 +252,7 @@ def _extract_tag_content(text: str, tag_name: str, stop_tags: List[str] | None =
251
252
  re.DOTALL | re.IGNORECASE,
252
253
  )
253
254
  if m:
254
- return m.group(1)
255
+ return html.unescape(m.group(1))
255
256
 
256
257
  # Strategy 3: Self-closing <tag/> or <tag />
257
258
  m = re.search(rf"<{tag_esc}[^>]*/\s*>", text, re.IGNORECASE)
@@ -268,7 +269,7 @@ def _extract_tag_content(text: str, tag_name: str, stop_tags: List[str] | None =
268
269
  content = m.group(1).strip()
269
270
  # Only return if there's actual content (not just whitespace)
270
271
  if content:
271
- return content
272
+ return html.unescape(content)
272
273
 
273
274
  return ""
274
275
 
@@ -300,7 +301,7 @@ def _extract_all_tag_blocks(
300
301
  re.DOTALL | re.IGNORECASE,
301
302
  )
302
303
  if properly_closed:
303
- return properly_closed
304
+ return [html.unescape(b) for b in properly_closed]
304
305
 
305
306
  # Conservative mode: only extract properly closed blocks
306
307
  if conservative:
@@ -326,7 +327,7 @@ def _extract_all_tag_blocks(
326
327
  else:
327
328
  content_end = len(text)
328
329
 
329
- blocks.append(text[content_start:content_end])
330
+ blocks.append(html.unescape(text[content_start:content_end]))
330
331
 
331
332
  return blocks
332
333
 
@@ -223,6 +223,8 @@ class ExecutionEngine:
223
223
  self._agent_name = agent_name
224
224
  # [v1.23.37] 当前正在运行的子进程引用,用于 cancel_current() 终止
225
225
  self._current_process: Optional[asyncio.subprocess.Process] = None
226
+ # [v1.23.38] 取消标志,用于在长等待中响应停止请求
227
+ self._cancelled = False
226
228
 
227
229
  # 安全: 合并正则模式 + 字符串黑名单
228
230
  self._blocked = set(self.DANGEROUS_COMMANDS)
@@ -508,6 +510,9 @@ class ExecutionEngine:
508
510
  work_dir = work_dir or self.work_dir
509
511
  metadata = metadata or {}
510
512
 
513
+ # [v1.23.38] 每次执行前重置取消标志
514
+ self._cancelled = False
515
+
511
516
  # 权限检查: execution 权限
512
517
  if not self._check_permission("execution"):
513
518
  return ExecResult(
@@ -623,7 +628,10 @@ class ExecutionEngine:
623
628
  if not chunk:
624
629
  break
625
630
  chunks.append(chunk)
626
- except (asyncio.CancelledError, Exception):
631
+ except asyncio.CancelledError:
632
+ # [v1.23.38] 传播取消信号,不再吞掉
633
+ raise
634
+ except Exception:
627
635
  pass
628
636
 
629
637
  # 同时启动 stdout 和 stderr 的读取任务
@@ -636,28 +644,63 @@ class ExecutionEngine:
636
644
 
637
645
  timed_out = False
638
646
  try:
639
- # 等待进程结束,同时流式收集输出
640
- done, pending = await asyncio.wait(
641
- [
642
- asyncio.ensure_future(process.wait()),
643
- stdout_task,
644
- stderr_task,
645
- ],
646
- timeout=timeout,
647
- )
648
- if pending:
649
- # 超时发生
650
- timed_out = True
651
- for t in pending:
652
- t.cancel()
653
- try:
654
- process.kill()
655
- except ProcessLookupError:
656
- pass
657
- # 等待取消完成,获取已缓冲的数据
658
- await asyncio.gather(*pending, return_exceptions=True)
659
-
660
- except Exception:
647
+ # [v1.23.38] 使用短间隔轮询,确保取消信号能及时响应
648
+ wait_timeout = min(timeout, 3.0) # 每3秒检查一次取消标志
649
+ remaining = timeout
650
+ while True:
651
+ if self._cancelled:
652
+ # 收到停止信号,终止进程
653
+ try:
654
+ process.kill()
655
+ except ProcessLookupError:
656
+ pass
657
+ for t in [stdout_task, stderr_task]:
658
+ if not t.done():
659
+ t.cancel()
660
+ await asyncio.gather(
661
+ stdout_task, stderr_task,
662
+ return_exceptions=True,
663
+ )
664
+ elapsed = asyncio.get_event_loop().time() - start_time
665
+ stdout_str = b"".join(stdout_chunks).decode("utf-8", errors="replace")
666
+ stderr_str = b"".join(stderr_chunks).decode("utf-8", errors="replace")
667
+ logger.info(f"[executor] 执行被用户停止 (elapsed={elapsed:.1f}s)")
668
+ return ExecResult(
669
+ success=False,
670
+ output=stdout_str,
671
+ error="执行被用户停止",
672
+ execution_time=elapsed,
673
+ metadata={"cancelled": True},
674
+ )
675
+ done, pending = await asyncio.wait(
676
+ [
677
+ asyncio.ensure_future(process.wait()),
678
+ stdout_task,
679
+ stderr_task,
680
+ ],
681
+ timeout=wait_timeout,
682
+ )
683
+ if process.returncode is not None:
684
+ # 进程已结束
685
+ if pending:
686
+ for t in pending:
687
+ t.cancel()
688
+ await asyncio.gather(*pending, return_exceptions=True)
689
+ break
690
+ remaining -= wait_timeout
691
+ if remaining <= 0:
692
+ # 超时
693
+ timed_out = True
694
+ for t in pending:
695
+ t.cancel()
696
+ try:
697
+ process.kill()
698
+ except ProcessLookupError:
699
+ pass
700
+ await asyncio.gather(*pending, return_exceptions=True)
701
+ break
702
+ except asyncio.CancelledError:
703
+ # [v1.23.38] asyncio.Task 被取消时传播
661
704
  timed_out = True
662
705
  try:
663
706
  process.kill()
@@ -670,6 +713,7 @@ class ExecutionEngine:
670
713
  stdout_task, stderr_task,
671
714
  return_exceptions=True,
672
715
  )
716
+ raise
673
717
 
674
718
  elapsed = asyncio.get_event_loop().time() - start_time
675
719
  stdout_str = b"".join(stdout_chunks).decode("utf-8", errors="replace")
@@ -998,6 +1042,7 @@ class ExecutionEngine:
998
1042
 
999
1043
  def cancel_current(self):
1000
1044
  """终止当前正在运行的子进程(由 /api/chat/stop 调用)"""
1045
+ self._cancelled = True # [v1.23.38] 设置取消标志
1001
1046
  proc = self._current_process
1002
1047
  if proc is None:
1003
1048
  return False
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.23.38",
3
+ "version": "1.23.40",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
@@ -43,4 +43,4 @@
43
43
  "python": ">=3.10",
44
44
  "node": ">=18"
45
45
  }
46
- }
46
+ }
package/web/api_server.py CHANGED
@@ -2175,6 +2175,15 @@ window.addEventListener('beforeunload', function() {{
2175
2175
  result_store["done"] = True
2176
2176
  await safe_write({"type": "done", "exec_events": exec_events_q, "session_id": session_id})
2177
2177
 
2178
+ except asyncio.CancelledError:
2179
+ # [v1.23.38] 任务被取消(用户点击停止),清理资源
2180
+ logger.info(f"[{session_id}] 后台流式任务被取消")
2181
+ result_store["done"] = True
2182
+ result_store["cancelled"] = True
2183
+ try:
2184
+ await safe_write({"type": "stopped", "session_id": session_id})
2185
+ except Exception:
2186
+ pass
2178
2187
  except Exception as e:
2179
2188
  logger.error(f"[{session_id}] 后台流式任务异常: {e}", exc_info=True)
2180
2189
  result_store["error"] = str(e)