myagent-ai 1.7.0 → 1.7.1
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/package.json +1 -1
- package/web/api_server.py +102 -10
package/package.json
CHANGED
package/web/api_server.py
CHANGED
|
@@ -2611,10 +2611,29 @@ class ApiServer:
|
|
|
2611
2611
|
max_iter = agent.config.agent.max_iterations if agent.config else 30
|
|
2612
2612
|
final_response = ""
|
|
2613
2613
|
iteration = 0
|
|
2614
|
+
# 追踪连续无 action 迭代次数,防止无限重新提示
|
|
2615
|
+
_consecutive_no_action = 0
|
|
2616
|
+
_MAX_NO_ACTION_RETRIES = 3
|
|
2614
2617
|
|
|
2615
2618
|
while iteration < max_iter:
|
|
2616
2619
|
iteration += 1
|
|
2617
2620
|
|
|
2621
|
+
# ── 执行模式:每轮迭代前刷新任务进度上下文 ──
|
|
2622
|
+
# 这样 LLM 在每次调用时都能看到最新的任务列表状态
|
|
2623
|
+
if chat_mode == "exec":
|
|
2624
|
+
fresh_task_context = self._build_task_plan_context(agent_path, chat_mode, user_message)
|
|
2625
|
+
if fresh_task_context:
|
|
2626
|
+
# 替换掉之前的任务规划上下文(保留非任务规划部分)
|
|
2627
|
+
base_prompt = context.metadata.get("agent_override_prompt", "")
|
|
2628
|
+
task_section_marker = "\n\n## 任务规划\n"
|
|
2629
|
+
idx = base_prompt.find(task_section_marker)
|
|
2630
|
+
if idx >= 0:
|
|
2631
|
+
# 替换旧的任务规划上下文
|
|
2632
|
+
context.metadata["agent_override_prompt"] = base_prompt[:idx] + task_section_marker + fresh_task_context
|
|
2633
|
+
else:
|
|
2634
|
+
# 没有旧的任务规划上下文,追加
|
|
2635
|
+
context.metadata["agent_override_prompt"] = base_prompt + task_section_marker + fresh_task_context
|
|
2636
|
+
|
|
2618
2637
|
# Build messages
|
|
2619
2638
|
messages = agent._build_messages(context)
|
|
2620
2639
|
tools = agent._get_tools()
|
|
@@ -2634,7 +2653,9 @@ class ApiServer:
|
|
|
2634
2653
|
}
|
|
2635
2654
|
|
|
2636
2655
|
# 需要回退(hold back)的最大字符数,用于检测 ```action 或 ```tasklist 标记
|
|
2637
|
-
|
|
2656
|
+
# 注意: 12 足够覆盖 ```tasklist\n (12字符) + 余量
|
|
2657
|
+
# 过大的值会导致短文本被 hold 住,流结束后一次性输出(看起来像非流式)
|
|
2658
|
+
_MAX_HOLD = 12
|
|
2638
2659
|
|
|
2639
2660
|
async def _text_delta_callback(full_text_so_far: str, delta_text: str):
|
|
2640
2661
|
"""智能流式过滤器:文本正常推送,JSON action 块拦截"""
|
|
@@ -2661,7 +2682,8 @@ class ApiServer:
|
|
|
2661
2682
|
text_before = remaining[:marker_pos]
|
|
2662
2683
|
if text_before.strip():
|
|
2663
2684
|
await _write_sse({"type": "text_delta", "content": text_before})
|
|
2664
|
-
|
|
2685
|
+
# 跳过整个开始标记(```action 或 ```tasklist),不要只跳到 ```
|
|
2686
|
+
st["processed_pos"] += marker_pos + len(f"```{block_type}")
|
|
2665
2687
|
if block_type == "tasklist":
|
|
2666
2688
|
st["mode"] = "tasklist_block"
|
|
2667
2689
|
else:
|
|
@@ -2706,10 +2728,14 @@ class ApiServer:
|
|
|
2706
2728
|
|
|
2707
2729
|
elif st["mode"] == "action_block":
|
|
2708
2730
|
# ── Action 代码块模式:寻找结束标记 ``` ──
|
|
2709
|
-
|
|
2731
|
+
# 结束标记是独立的 ``` 行(前面有换行),不要匹配块开始标记内的 ```
|
|
2732
|
+
end_marker = remaining.find("\n```")
|
|
2733
|
+
if end_marker < 0 and remaining.startswith("```"):
|
|
2734
|
+
end_marker = 0
|
|
2710
2735
|
if end_marker >= 0:
|
|
2711
2736
|
# 找到结束标记,跳过整个 action 块(不推送)
|
|
2712
|
-
|
|
2737
|
+
skip_len = (end_marker + 1 + 3) if end_marker > 0 else 3
|
|
2738
|
+
st["processed_pos"] += skip_len
|
|
2713
2739
|
st["mode"] = "text"
|
|
2714
2740
|
remaining = full_text_so_far[st["processed_pos"]:]
|
|
2715
2741
|
continue
|
|
@@ -2719,10 +2745,13 @@ class ApiServer:
|
|
|
2719
2745
|
|
|
2720
2746
|
elif st["mode"] == "tasklist_block":
|
|
2721
2747
|
# ── Tasklist 代码块模式:寻找结束标记 ``` ──
|
|
2722
|
-
end_marker = remaining.find("```")
|
|
2748
|
+
end_marker = remaining.find("\n```")
|
|
2749
|
+
if end_marker < 0 and remaining.startswith("```"):
|
|
2750
|
+
end_marker = 0
|
|
2723
2751
|
if end_marker >= 0:
|
|
2724
2752
|
# 找到结束标记,跳过整个 tasklist 块(不推送)
|
|
2725
|
-
|
|
2753
|
+
skip_len = (end_marker + 1 + 3) if end_marker > 0 else 3
|
|
2754
|
+
st["processed_pos"] += skip_len
|
|
2726
2755
|
st["mode"] = "text"
|
|
2727
2756
|
remaining = full_text_so_far[st["processed_pos"]:]
|
|
2728
2757
|
continue
|
|
@@ -2756,7 +2785,11 @@ class ApiServer:
|
|
|
2756
2785
|
st = _stream_state
|
|
2757
2786
|
remaining = full_text[st["processed_pos"]:]
|
|
2758
2787
|
if remaining.strip() and st["mode"] == "text":
|
|
2759
|
-
|
|
2788
|
+
# 如果剩余文本较长,逐 token 推送以保持流式体验
|
|
2789
|
+
if len(remaining) > 20:
|
|
2790
|
+
await _stream_text_chunked(remaining, _write_sse, chunk_size=3, delay=0.01)
|
|
2791
|
+
else:
|
|
2792
|
+
await _write_sse({"type": "text_delta", "content": remaining})
|
|
2760
2793
|
st["processed_pos"] = len(full_text)
|
|
2761
2794
|
|
|
2762
2795
|
# Call LLM with streaming — tokens are filtered through _text_delta_callback
|
|
@@ -2825,6 +2858,8 @@ class ApiServer:
|
|
|
2825
2858
|
)
|
|
2826
2859
|
continue # Next iteration — let LLM process tool results
|
|
2827
2860
|
|
|
2861
|
+
# 注意:成功处理 tool_calls 或 actions 后,_consecutive_no_action 会被重置
|
|
2862
|
+
|
|
2828
2863
|
# ── 从混合内容中提取 JSON action 指令 ──
|
|
2829
2864
|
# 支持格式:
|
|
2830
2865
|
# 1. 纯 JSON(向后兼容)
|
|
@@ -2945,6 +2980,7 @@ class ApiServer:
|
|
|
2945
2980
|
|
|
2946
2981
|
# Continue the loop — let LLM decide next step based on execution results
|
|
2947
2982
|
# tasklist 已在循环顶部 LLM 调用后提取并推送,此处无需重复
|
|
2983
|
+
_consecutive_no_action = 0 # 成功执行 action,重置无 action 计数
|
|
2948
2984
|
|
|
2949
2985
|
continue
|
|
2950
2986
|
|
|
@@ -2956,10 +2992,66 @@ class ApiServer:
|
|
|
2956
2992
|
await _stream_text_chunked(final_response, _write_sse)
|
|
2957
2993
|
break
|
|
2958
2994
|
|
|
2959
|
-
# ── Pure text response (no actions/tool calls)
|
|
2995
|
+
# ── Pure text response (no actions/tool calls) ──
|
|
2960
2996
|
# Content was already streamed token-by-token via _text_delta_callback
|
|
2961
|
-
|
|
2962
|
-
|
|
2997
|
+
|
|
2998
|
+
# ── 执行模式多轮续命机制 ──
|
|
2999
|
+
# 问题:LLM 有时只输出文字总结而不输出 ```action``` 块,
|
|
3000
|
+
# 导致循环在此处 break,任务未完成就停止。
|
|
3001
|
+
# 修复:检查任务列表中是否还有未完成步骤,如果有则重新提示 LLM 继续。
|
|
3002
|
+
if chat_mode == "exec":
|
|
3003
|
+
current_tasks = self._task_list_store.get(agent_path, [])
|
|
3004
|
+
pending_count = sum(
|
|
3005
|
+
1 for t in current_tasks
|
|
3006
|
+
if t.get("status") in ("pending", "running", "blocked")
|
|
3007
|
+
)
|
|
3008
|
+
if pending_count > 0 and _consecutive_no_action < _MAX_NO_ACTION_RETRIES and iteration < max_iter - 1:
|
|
3009
|
+
# 还有未完成任务,LLM 忘记了输出 action 块,重新提示
|
|
3010
|
+
_consecutive_no_action += 1
|
|
3011
|
+
logger.info(
|
|
3012
|
+
f"[{session_id}] Exec 模式: LLM 未输出 action,"
|
|
3013
|
+
f"重新提示继续执行 ({_consecutive_no_action}/{_MAX_NO_ACTION_RETRIES})"
|
|
3014
|
+
)
|
|
3015
|
+
context.conversation_history.append(
|
|
3016
|
+
Message(role="assistant", content=content)
|
|
3017
|
+
)
|
|
3018
|
+
context.conversation_history.append(
|
|
3019
|
+
Message(
|
|
3020
|
+
role="user",
|
|
3021
|
+
content=(
|
|
3022
|
+
f"[系统提示] 任务尚未完成,仍有 {pending_count} 个未执行步骤。"
|
|
3023
|
+
"你必须继续执行操作:用 ```action``` 代码块输出操作指令。"
|
|
3024
|
+
"不要只输出文字总结或反馈,必须输出具体的操作命令。"
|
|
3025
|
+
),
|
|
3026
|
+
)
|
|
3027
|
+
)
|
|
3028
|
+
await _write_sse({
|
|
3029
|
+
"type": "thought",
|
|
3030
|
+
"content": (
|
|
3031
|
+
f"\n\n⏳ 检测到 {pending_count} 个未完成任务,"
|
|
3032
|
+
f"继续执行下一步操作..."
|
|
3033
|
+
),
|
|
3034
|
+
})
|
|
3035
|
+
# 清除上一轮的流式输出,为下一轮腾出空间
|
|
3036
|
+
await _write_sse({"type": "clear_text"})
|
|
3037
|
+
continue
|
|
3038
|
+
else:
|
|
3039
|
+
# 所有任务完成,或已达到重试上限
|
|
3040
|
+
if pending_count > 0 and _consecutive_no_action >= _MAX_NO_ACTION_RETRIES:
|
|
3041
|
+
logger.warning(
|
|
3042
|
+
f"[{session_id}] Exec 模式: 连续 {_MAX_NO_ACTION_RETRIES} 次"
|
|
3043
|
+
f"无 action 输出,强制结束循环"
|
|
3044
|
+
)
|
|
3045
|
+
# 在回复末尾追加提示
|
|
3046
|
+
final_response = content + (
|
|
3047
|
+
f"\n\n⚠️ 注意:仍有 {pending_count} 个任务未完成,"
|
|
3048
|
+
"但 Agent 已达到最大重试次数。你可以要求我继续完成这些任务。"
|
|
3049
|
+
)
|
|
3050
|
+
break
|
|
3051
|
+
else:
|
|
3052
|
+
# 非 exec 模式或无待办任务,正常结束
|
|
3053
|
+
final_response = content
|
|
3054
|
+
break
|
|
2963
3055
|
|
|
2964
3056
|
# Save assistant response to memory
|
|
2965
3057
|
if agent.memory and final_response:
|