Merge pull request #2651 from zhayujie/feat-cow-agent

fix: optimize suggestion words and retries
This commit is contained in:
zhayujie
2026-02-01 14:11:53 +08:00
committed by GitHub
9 changed files with 216 additions and 148 deletions

View File

@@ -456,7 +456,7 @@ class MemoryManager:
- 当天笔记 → memory/{today_file}
- 静默存储,仅在明确要求时确认
**使用原则**: 自然使用记忆,就像你本来就知道。不要主动提起或列举记忆,除非用户明确询问"""
**使用原则**: 自然使用记忆,就像你本来就知道。不需要生硬地提起或列举记忆,除非用户提到"""
else:
guidance = f"""## Memory System

View File

@@ -1,7 +1,7 @@
"""
System Prompt Builder - 系统提示词构建器
参考 clawdbot 的 system-prompt.ts实现中文版的模块化提示词构建
实现模块化的系统提示词构建,支持工具、技能、记忆等多个子系统
"""
import os
@@ -90,22 +90,21 @@ def build_agent_system_prompt(
**kwargs
) -> str:
"""
构建Agent系统提示词(精简版,中文)
构建Agent系统提示词
包含的sections:
1. 基础身份
2. 工具说明
3. 技能系统
4. 记忆系统
5. 用户身份
6. 文档路径
7. 工作空间
8. 项目上下文文件
顺序说明(按重要性和逻辑关系排列):
1. 工具系统 - 核心能力,最先介绍
2. 技能系统 - 紧跟工具,因为技能需要用 read 工具读取
3. 记忆系统 - 独立的记忆能力
4. 工作空间 - 工作环境说明
5. 用户身份 - 用户信息(可选)
6. 项目上下文 - SOUL.md, USER.md, AGENTS.md定义人格和身份
7. 运行时信息 - 元信息(时间、模型等)
Args:
workspace_dir: 工作空间目录
language: 语言 ("zh""en")
base_persona: 基础人格描述
base_persona: 基础人格描述已废弃由SOUL.md定义
user_identity: 用户身份信息
tools: 工具列表
context_files: 上下文文件列表
@@ -120,33 +119,30 @@ def build_agent_system_prompt(
"""
sections = []
# 1. 基础身份
sections.extend(_build_identity_section(base_persona, language))
# 2. 工具说明
# 1. 工具系统(最重要,放在最前面)
if tools:
sections.extend(_build_tooling_section(tools, language))
# 3. 技能系统
# 2. 技能系统(紧跟工具,因为需要用 read 工具)
if skill_manager:
sections.extend(_build_skills_section(skill_manager, tools, language))
# 4. 记忆系统
# 3. 记忆系统(独立的记忆能力)
if memory_manager:
sections.extend(_build_memory_section(memory_manager, tools, language))
# 5. 用户身份
# 4. 工作空间(工作环境说明)
sections.extend(_build_workspace_section(workspace_dir, language, is_first_conversation))
# 5. 用户身份(如果有)
if user_identity:
sections.extend(_build_user_identity_section(user_identity, language))
# 6. 工作空间
sections.extend(_build_workspace_section(workspace_dir, language, is_first_conversation))
# 7. 项目上下文文件SOUL.md, USER.md等
# 6. 项目上下文文件SOUL.md, USER.md, AGENTS.md - 定义人格)
if context_files:
sections.extend(_build_context_files_section(context_files, language))
# 8. 运行时信息(如果有
# 7. 运行时信息(元信息,放在最后
if runtime_info:
sections.extend(_build_runtime_section(runtime_info, language))
@@ -270,9 +266,9 @@ def _build_skills_section(skill_manager: Any, tools: Optional[List[Any]], langua
"",
"在回复之前:扫描下方 <available_skills> 中的 <description> 条目。",
"",
f"- 如果恰好有一个技能明确适用:使用 `{read_tool_name}` 工具读取其 <location> 路径下的 SKILL.md 文件,然后遵循它",
"- 如果多个技能都适用:选择最具体的一个,然后读取并遵循",
"- 如果没有明确适用的:不要读取任何 SKILL.md",
f"- 如果恰好有一个技能明确适用:使用 `{read_tool_name}` 工具读取其 <location> 路径下的 SKILL.md 文件,然后遵循它",
"- 如果多个技能都适用:选择最具体的一个,然后读取并遵循",
"- 如果没有明确适用的:不要读取任何 SKILL.md",
"",
"**约束**: 永远不要一次性读取多个技能;只在选择后再读取。",
"",
@@ -455,7 +451,27 @@ def _build_runtime_section(runtime_info: Dict[str, Any], language: str) -> List[
if not runtime_info:
return []
# Only include if there's actual runtime info to display
lines = [
"## 运行时信息",
"",
]
# Add current time if available
if runtime_info.get("current_time"):
time_str = runtime_info["current_time"]
weekday = runtime_info.get("weekday", "")
timezone = runtime_info.get("timezone", "")
time_line = f"当前时间: {time_str}"
if weekday:
time_line += f" {weekday}"
if timezone:
time_line += f" ({timezone})"
lines.append(time_line)
lines.append("")
# Add other runtime info
runtime_parts = []
if runtime_info.get("model"):
runtime_parts.append(f"模型={runtime_info['model']}")
@@ -465,14 +481,8 @@ def _build_runtime_section(runtime_info: Dict[str, Any], language: str) -> List[
if runtime_info.get("channel") and runtime_info.get("channel") != "web":
runtime_parts.append(f"渠道={runtime_info['channel']}")
if not runtime_parts:
return []
lines = [
"## 运行时信息",
"",
"运行时: " + " | ".join(runtime_parts),
""
]
if runtime_parts:
lines.append("运行时: " + " | ".join(runtime_parts))
lines.append("")
return lines

View File

@@ -236,24 +236,9 @@ def _get_agents_template() -> str:
这个文件夹是你的家。好好对待它。
## 系统自动加载
以下文件在每次会话启动时**已经自动加载**到系统提示词中,你无需再次读取:
- ✅ `SOUL.md` - 你的人格设定(已加载)
- ✅ `USER.md` - 用户信息(已加载)
- ✅ `AGENTS.md` - 本文件(已加载)
## 按需读取
以下文件**不会自动加载**,需要时使用相应工具读取:
- 📝 `memory/YYYY-MM-DD.md` - 每日记忆(用 memory_search 检索)
- 🧠 `MEMORY.md` - 长期记忆(用 memory_search 检索)
## 记忆系统
你每次会话都是全新的。这些文件是你的连续性:
你每次会话都是全新的,记忆文件让你保持连续性:
### 📝 每日记忆:`memory/YYYY-MM-DD.md`
- 原始的对话日志
@@ -296,13 +281,9 @@ def _get_agents_template() -> str:
- 不要在未经询问的情况下运行破坏性命令
- 当有疑问时,先问
## 工具使用
## 工作空间演化
技能提供你的工具。当你需要一个时,查看它的 `SKILL.md`
## 让它成为你的
这只是一个起点。随着你弄清楚什么有效,添加你自己的约定、风格和规则。
这个工作空间会随着你的使用而不断成长。当你学到新东西、发现更好的方式,或者犯错后改正时,记录下来
"""

View File

@@ -95,15 +95,16 @@ class Agent:
"""
Get the full system prompt including skills.
:param skill_filter: Optional list of skill names to include
:return: Complete system prompt with skills appended
"""
base_prompt = self.system_prompt
skills_prompt = self.get_skills_prompt(skill_filter=skill_filter)
Note: Skills are now built into the system prompt by PromptBuilder,
so we just return the base prompt directly. This method is kept for
backward compatibility.
if skills_prompt:
return base_prompt + "\n" + skills_prompt
return base_prompt
:param skill_filter: Optional list of skill names to include (deprecated)
:return: Complete system prompt
"""
# Skills are now included in system_prompt by PromptBuilder
# No need to append them here
return self.system_prompt
def refresh_skills(self):
"""Refresh the loaded skills."""

View File

@@ -55,6 +55,9 @@ class AgentStreamExecutor:
# Message history - use provided messages or create new list
self.messages = messages if messages is not None else []
# Tool failure tracking for retry protection
self.tool_failure_history = [] # List of (tool_name, args_hash, success) tuples
def _emit_event(self, event_type: str, data: dict = None):
"""Emit event"""
@@ -68,6 +71,60 @@ class AgentStreamExecutor:
except Exception as e:
logger.error(f"Event callback error: {e}")
def _hash_args(self, args: dict) -> str:
"""Generate a simple hash for tool arguments"""
import hashlib
# Sort keys for consistent hashing
args_str = json.dumps(args, sort_keys=True, ensure_ascii=False)
return hashlib.md5(args_str.encode()).hexdigest()[:8]
def _check_consecutive_failures(self, tool_name: str, args: dict) -> tuple[bool, str]:
"""
Check if tool has failed too many times consecutively
Returns:
(should_stop, reason)
"""
args_hash = self._hash_args(args)
# Count consecutive failures for same tool + args
same_args_failures = 0
for name, ahash, success in reversed(self.tool_failure_history):
if name == tool_name and ahash == args_hash:
if not success:
same_args_failures += 1
else:
break # Stop at first success
else:
break # Different tool or args, stop counting
if same_args_failures >= 3:
return True, f"Tool '{tool_name}' with same arguments failed {same_args_failures} times consecutively. Stopping to prevent infinite loop."
# Count consecutive failures for same tool (any args)
same_tool_failures = 0
for name, ahash, success in reversed(self.tool_failure_history):
if name == tool_name:
if not success:
same_tool_failures += 1
else:
break # Stop at first success
else:
break # Different tool, stop counting
if same_tool_failures >= 6:
return True, f"Tool '{tool_name}' failed {same_tool_failures} times consecutively (with any arguments). Stopping to prevent infinite loop."
return False, ""
def _record_tool_result(self, tool_name: str, args: dict, success: bool):
"""Record tool execution result for failure tracking"""
args_hash = self._hash_args(args)
self.tool_failure_history.append((tool_name, args_hash, success))
# Keep only last 50 records to avoid memory bloat
if len(self.tool_failure_history) > 50:
self.tool_failure_history = self.tool_failure_history[-50:]
def run_stream(self, user_message: str) -> str:
"""
Execute streaming reasoning loop
@@ -413,7 +470,16 @@ class AgentStreamExecutor:
arguments = json.loads(tc["arguments"]) if tc["arguments"] else {}
except json.JSONDecodeError as e:
logger.error(f"Failed to parse tool arguments: {tc['arguments']}")
arguments = {}
logger.error(f"JSON decode error: {e}")
# Return a clear error message to the LLM instead of empty dict
# This helps the LLM understand what went wrong
tool_calls.append({
"id": tc["id"],
"name": tc["name"],
"arguments": {},
"_parse_error": f"Invalid JSON in tool arguments: {tc['arguments'][:100]}... Error: {str(e)}"
})
continue
tool_calls.append({
"id": tc["id"],
@@ -481,6 +547,31 @@ class AgentStreamExecutor:
tool_id = tool_call["id"]
arguments = tool_call["arguments"]
# Check if there was a JSON parse error
if "_parse_error" in tool_call:
parse_error = tool_call["_parse_error"]
logger.error(f"Skipping tool execution due to parse error: {parse_error}")
result = {
"status": "error",
"result": f"Failed to parse tool arguments. {parse_error}. Please ensure your tool call uses valid JSON format with all required parameters.",
"execution_time": 0
}
self._record_tool_result(tool_name, arguments, False)
return result
# Check for consecutive failures (retry protection)
should_stop, stop_reason = self._check_consecutive_failures(tool_name, arguments)
if should_stop:
logger.error(f"🛑 {stop_reason}")
self._record_tool_result(tool_name, arguments, False)
# 返回错误给 LLM,让它尝试其他方法
result = {
"status": "error",
"result": f"{stop_reason}\n\nThis approach is not working. Please try a completely different method or ask the user for more information/clarification.",
"execution_time": 0
}
return result
self._emit_event("tool_execution_start", {
"tool_call_id": tool_id,
"tool_name": tool_name,
@@ -507,6 +598,10 @@ class AgentStreamExecutor:
"execution_time": execution_time
}
# Record tool result for failure tracking
success = result.status == "success"
self._record_tool_result(tool_name, arguments, success)
# Auto-refresh skills after skill creation
if tool_name == "bash" and result.status == "success":
command = arguments.get("command", "")
@@ -530,6 +625,9 @@ class AgentStreamExecutor:
"result": str(e),
"execution_time": 0
}
# Record failure
self._record_tool_result(tool_name, arguments, False)
self._emit_event("tool_execution_end", {
"tool_call_id": tool_id,
"tool_name": tool_name,

View File

@@ -4,7 +4,6 @@ from agent.tools.tool_manager import ToolManager
# Import basic tools (no external dependencies)
from agent.tools.calculator.calculator import Calculator
from agent.tools.current_time.current_time import CurrentTime
# Import file operation tools
from agent.tools.read.read import Read
@@ -82,7 +81,6 @@ __all__ = [
'BaseTool',
'ToolManager',
'Calculator',
'CurrentTime',
'Read',
'Write',
'Edit',

View File

@@ -1,75 +0,0 @@
import datetime
import time
from agent.tools.base_tool import BaseTool, ToolResult
class CurrentTime(BaseTool):
name: str = "time"
description: str = "A tool to get current date and time information."
params: dict = {
"type": "object",
"properties": {
"format": {
"type": "string",
"description": "Optional format for the time (e.g., 'iso', 'unix', 'human'). Default is 'human'."
},
"timezone": {
"type": "string",
"description": "Optional timezone specification (e.g., 'UTC', 'local'). Default is 'local'."
}
},
"required": []
}
config: dict = {}
def execute(self, args: dict) -> ToolResult:
try:
# Get the format and timezone parameters, with defaults
time_format = args.get("format", "human").lower()
timezone = args.get("timezone", "local").lower()
# Get current time
current_time = datetime.datetime.now()
# Handle timezone if specified
if timezone == "utc":
current_time = datetime.datetime.utcnow()
# Format the time according to the specified format
if time_format == "iso":
# ISO 8601 format
formatted_time = current_time.isoformat()
elif time_format == "unix":
# Unix timestamp (seconds since epoch)
formatted_time = time.time()
else:
# Human-readable format
formatted_time = current_time.strftime("%Y-%m-%d %H:%M:%S")
# Prepare additional time components for the response
year = current_time.year
month = current_time.month
day = current_time.day
hour = current_time.hour
minute = current_time.minute
second = current_time.second
weekday = current_time.strftime("%A") # Full weekday name
result = {
"current_time": formatted_time,
"components": {
"year": year,
"month": month,
"day": day,
"hour": hour,
"minute": minute,
"second": second,
"weekday": weekday
},
"format": time_format,
"timezone": timezone
}
return ToolResult.success(result=result)
except Exception as e:
return ToolResult.fail(result=str(e))

View File

@@ -473,6 +473,15 @@ class AgentBridge:
# Load context files
context_files = load_context_files(workspace_root)
# Initialize skill manager
skill_manager = None
try:
from agent.skills import SkillManager
skill_manager = SkillManager(workspace_dir=workspace_root)
logger.info(f"[AgentBridge] Initialized SkillManager with {len(skill_manager.skills)} skills for session {session_id}")
except Exception as e:
logger.warning(f"[AgentBridge] Failed to initialize SkillManager for session {session_id}: {e}")
# Check if this is the first conversation
from agent.prompt.workspace import is_first_conversation, mark_conversation_started
is_first = is_first_conversation(workspace_root)
@@ -483,15 +492,49 @@ class AgentBridge:
language="zh"
)
# Get current time and timezone info
import datetime
import time
now = datetime.datetime.now()
# Get timezone info
try:
offset = -time.timezone if not time.daylight else -time.altzone
hours = offset // 3600
minutes = (offset % 3600) // 60
if minutes:
timezone_name = f"UTC{hours:+03d}:{minutes:02d}"
else:
timezone_name = f"UTC{hours:+03d}"
except Exception:
timezone_name = "UTC"
# Chinese weekday mapping
weekday_map = {
'Monday': '星期一',
'Tuesday': '星期二',
'Wednesday': '星期三',
'Thursday': '星期四',
'Friday': '星期五',
'Saturday': '星期六',
'Sunday': '星期日'
}
weekday_zh = weekday_map.get(now.strftime("%A"), now.strftime("%A"))
runtime_info = {
"model": conf().get("model", "unknown"),
"workspace": workspace_root,
"channel": conf().get("channel_type", "unknown")
"channel": conf().get("channel_type", "unknown"),
"current_time": now.strftime("%Y-%m-%d %H:%M:%S"),
"weekday": weekday_zh,
"timezone": timezone_name
}
system_prompt = prompt_builder.build(
tools=tools,
context_files=context_files,
skill_manager=skill_manager,
memory_manager=memory_manager,
runtime_info=runtime_info,
is_first_conversation=is_first
@@ -507,6 +550,7 @@ class AgentBridge:
max_steps=50,
output_mode="logger",
workspace_dir=workspace_root,
skill_manager=skill_manager,
enable_skills=True
)

View File

@@ -285,7 +285,18 @@ class FeiShuChanel(ChatChannel):
context["origin_ctype"] = ctype
cmsg = context["msg"]
context["session_id"] = cmsg.from_user_id
# Set session_id based on chat type to ensure proper session isolation
if cmsg.is_group:
# Group chat: combine user_id and group_id to create unique session per user per group
# This ensures:
# - Same user in different groups have separate conversation histories
# - Same user in private chat and group chat have separate histories
context["session_id"] = f"{cmsg.from_user_id}:{cmsg.other_user_id}"
else:
# Private chat: use user_id only
context["session_id"] = cmsg.from_user_id
context["receiver"] = cmsg.other_user_id
if ctype == ContextType.TEXT: