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 +3 -0
- package/agents/main_agent.py +15 -3
- package/core/output_parser.py +6 -5
- package/executor/engine.py +68 -23
- package/package.json +2 -2
- package/web/api_server.py +9 -0
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))
|
package/agents/main_agent.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
package/core/output_parser.py
CHANGED
|
@@ -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
|
|
package/executor/engine.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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.
|
|
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)
|