fix: tool call failed problem

This commit is contained in:
zhayujie
2026-02-01 12:31:58 +08:00
parent 07959a3bff
commit 9bf5b0fc48
7 changed files with 331 additions and 85 deletions

View File

@@ -171,54 +171,75 @@ class AgentStreamExecutor:
tool_results = []
tool_result_blocks = []
for tool_call in tool_calls:
result = self._execute_tool(tool_call)
tool_results.append(result)
# Log tool result in compact format
status_emoji = "" if result.get("status") == "success" else ""
result_data = result.get('result', '')
# Format result string with proper Chinese character support
if isinstance(result_data, (dict, list)):
result_str = json.dumps(result_data, ensure_ascii=False)
else:
result_str = str(result_data)
logger.info(f" {status_emoji} {tool_call['name']} ({result.get('execution_time', 0):.2f}s): {result_str[:200]}{'...' if len(result_str) > 200 else ''}")
try:
for tool_call in tool_calls:
result = self._execute_tool(tool_call)
tool_results.append(result)
# Log tool result in compact format
status_emoji = "" if result.get("status") == "success" else ""
result_data = result.get('result', '')
# Format result string with proper Chinese character support
if isinstance(result_data, (dict, list)):
result_str = json.dumps(result_data, ensure_ascii=False)
else:
result_str = str(result_data)
logger.info(f" {status_emoji} {tool_call['name']} ({result.get('execution_time', 0):.2f}s): {result_str[:200]}{'...' if len(result_str) > 200 else ''}")
# Build tool result block (Claude format)
# Format content in a way that's easy for LLM to understand
is_error = result.get("status") == "error"
if is_error:
# For errors, provide clear error message
result_content = f"Error: {result.get('result', 'Unknown error')}"
elif isinstance(result.get('result'), dict):
# For dict results, use JSON format
result_content = json.dumps(result.get('result'), ensure_ascii=False)
elif isinstance(result.get('result'), str):
# For string results, use directly
result_content = result.get('result')
else:
# Fallback to full JSON
result_content = json.dumps(result, ensure_ascii=False)
tool_result_block = {
"type": "tool_result",
"tool_use_id": tool_call["id"],
"content": result_content
}
# Add is_error field for Claude API (helps model understand failures)
if is_error:
tool_result_block["is_error"] = True
tool_result_blocks.append(tool_result_block)
# Add tool results to message history as user message (Claude format)
self.messages.append({
"role": "user",
"content": tool_result_blocks
})
# Build tool result block (Claude format)
# Format content in a way that's easy for LLM to understand
is_error = result.get("status") == "error"
if is_error:
# For errors, provide clear error message
result_content = f"Error: {result.get('result', 'Unknown error')}"
elif isinstance(result.get('result'), dict):
# For dict results, use JSON format
result_content = json.dumps(result.get('result'), ensure_ascii=False)
elif isinstance(result.get('result'), str):
# For string results, use directly
result_content = result.get('result')
else:
# Fallback to full JSON
result_content = json.dumps(result, ensure_ascii=False)
tool_result_block = {
"type": "tool_result",
"tool_use_id": tool_call["id"],
"content": result_content
}
# Add is_error field for Claude API (helps model understand failures)
if is_error:
tool_result_block["is_error"] = True
tool_result_blocks.append(tool_result_block)
finally:
# CRITICAL: Always add tool_result to maintain message history integrity
# Even if tool execution fails, we must add error results to match tool_use
if tool_result_blocks:
# Add tool results to message history as user message (Claude format)
self.messages.append({
"role": "user",
"content": tool_result_blocks
})
elif tool_calls:
# If we have tool_calls but no tool_result_blocks (unexpected error),
# create error results for all tool calls to maintain message integrity
logger.warning("⚠️ Tool execution interrupted, adding error results to maintain message history")
emergency_blocks = []
for tool_call in tool_calls:
emergency_blocks.append({
"type": "tool_result",
"tool_use_id": tool_call["id"],
"content": "Error: Tool execution was interrupted",
"is_error": True
})
self.messages.append({
"role": "user",
"content": emergency_blocks
})
self._emit_event("turn_end", {
"turn": turn,
@@ -257,6 +278,9 @@ class AgentStreamExecutor:
Returns:
(response_text, tool_calls)
"""
# Validate and fix message history first
self._validate_and_fix_messages()
# Trim messages if needed (using agent's context management)
self._trim_messages()
@@ -513,6 +537,27 @@ class AgentStreamExecutor:
})
return error_result
def _validate_and_fix_messages(self):
"""
Validate message history and fix incomplete tool_use/tool_result pairs.
Claude API requires each tool_use to have a corresponding tool_result immediately after.
"""
if not self.messages:
return
# Check last message for incomplete tool_use
if len(self.messages) > 0:
last_msg = self.messages[-1]
if last_msg.get("role") == "assistant":
# Check if assistant message has tool_use blocks
content = last_msg.get("content", [])
if isinstance(content, list):
has_tool_use = any(block.get("type") == "tool_use" for block in content)
if has_tool_use:
# This is incomplete - remove it
logger.warning(f"⚠️ Removing incomplete tool_use message from history")
self.messages.pop()
def _trim_messages(self):
"""
Trim message history to stay within context limits.

View File

@@ -21,6 +21,7 @@ from agent.tools.memory.memory_get import MemoryGetTool
# Import web tools
from agent.tools.web_fetch.web_fetch import WebFetch
from agent.tools.bocha_search.bocha_search import BochaSearch
# Import tools with optional dependencies
def _import_optional_tools():
@@ -93,6 +94,7 @@ __all__ = [
'MemorySearchTool',
'MemoryGetTool',
'WebFetch',
'BochaSearch',
# Optional tools (may be None if dependencies not available)
'GoogleSearch',
'FileSave',

View File

@@ -46,7 +46,7 @@ class WebFetch(BaseTool):
def __init__(self, config: dict = None):
self.config = config or {}
self.timeout = self.config.get("timeout", 30)
self.timeout = self.config.get("timeout", 20)
self.max_redirects = self.config.get("max_redirects", 3)
self.user_agent = self.config.get(
"user_agent",

View File

@@ -171,13 +171,15 @@ class AgentLLMModel(LLMModel):
class AgentBridge:
"""
Bridge class that integrates single super Agent with COW
Bridge class that integrates super Agent with COW
Manages multiple agent instances per session for conversation isolation
"""
def __init__(self, bridge: Bridge):
self.bridge = bridge
self.agents = {} # session_id -> Agent instance mapping
self.default_agent = None # For backward compatibility (no session_id)
self.agent: Optional[Agent] = None
def create_agent(self, system_prompt: str, tools: List = None, **kwargs) -> Agent:
"""
Create the super agent with COW integration
@@ -209,8 +211,8 @@ class AgentBridge:
except Exception as e:
logger.warning(f"[AgentBridge] Failed to load tool {tool_name}: {e}")
# Create the single super agent
self.agent = Agent(
# Create agent instance
agent = Agent(
system_prompt=system_prompt,
description=kwargs.get("description", "AI Super Agent"),
model=model,
@@ -225,21 +227,38 @@ class AgentBridge:
)
# Log skill loading details
if self.agent.skill_manager:
if agent.skill_manager:
logger.info(f"[AgentBridge] SkillManager initialized:")
logger.info(f"[AgentBridge] - Managed dir: {self.agent.skill_manager.managed_skills_dir}")
logger.info(f"[AgentBridge] - Workspace dir: {self.agent.skill_manager.workspace_dir}")
logger.info(f"[AgentBridge] - Total skills: {len(self.agent.skill_manager.skills)}")
for skill_name in self.agent.skill_manager.skills.keys():
logger.info(f"[AgentBridge] - Managed dir: {agent.skill_manager.managed_skills_dir}")
logger.info(f"[AgentBridge] - Workspace dir: {agent.skill_manager.workspace_dir}")
logger.info(f"[AgentBridge] - Total skills: {len(agent.skill_manager.skills)}")
for skill_name in agent.skill_manager.skills.keys():
logger.info(f"[AgentBridge] * {skill_name}")
return self.agent
return agent
def get_agent(self) -> Optional[Agent]:
"""Get the super agent, create if not exists"""
if self.agent is None:
self._init_default_agent()
return self.agent
def get_agent(self, session_id: str = None) -> Optional[Agent]:
"""
Get agent instance for the given session
Args:
session_id: Session identifier (e.g., user_id). If None, returns default agent.
Returns:
Agent instance for this session
"""
# If no session_id, use default agent (backward compatibility)
if session_id is None:
if self.default_agent is None:
self._init_default_agent()
return self.default_agent
# Check if agent exists for this session
if session_id not in self.agents:
logger.info(f"[AgentBridge] Creating new agent for session: {session_id}")
self._init_agent_for_session(session_id)
return self.agents[session_id]
def _init_default_agent(self):
"""Initialize default super agent with new prompt system"""
@@ -307,6 +326,12 @@ class AgentBridge:
tool.cwd = file_config.get("cwd", tool.cwd if hasattr(tool, 'cwd') else None)
if 'memory_manager' in file_config:
tool.memory_manager = file_config['memory_manager']
# Apply API key for bocha_search tool
elif tool_name == 'bocha_search':
bocha_api_key = conf().get("bocha_api_key", "")
if bocha_api_key:
tool.config = {"bocha_api_key": bocha_api_key}
tool.api_key = bocha_api_key
tools.append(tool)
logger.debug(f"[AgentBridge] Loaded tool: {tool_name}")
except Exception as e:
@@ -370,6 +395,127 @@ class AgentBridge:
if memory_manager:
agent.memory_manager = memory_manager
logger.info(f"[AgentBridge] Memory manager attached to agent")
# Store as default agent
self.default_agent = agent
def _init_agent_for_session(self, session_id: str):
"""
Initialize agent for a specific session
Reuses the same configuration as default agent
"""
from config import conf
import os
# Get workspace from config
workspace_root = os.path.expanduser(conf().get("agent_workspace", "~/cow"))
# Initialize workspace
from agent.prompt import ensure_workspace, load_context_files, PromptBuilder
workspace_files = ensure_workspace(workspace_root, create_templates=True)
# Setup memory system
memory_manager = None
memory_tools = []
try:
from agent.memory import MemoryManager, MemoryConfig
from agent.tools import MemorySearchTool, MemoryGetTool
memory_config = MemoryConfig(
workspace_root=workspace_root,
embedding_provider="local",
embedding_model="all-MiniLM-L6-v2"
)
memory_manager = MemoryManager(memory_config)
memory_tools = [
MemorySearchTool(memory_manager),
MemoryGetTool(memory_manager)
]
except Exception as e:
logger.debug(f"[AgentBridge] Memory system not available for session {session_id}: {e}")
# Load tools
from agent.tools import ToolManager
tool_manager = ToolManager()
tool_manager.load_tools()
tools = []
file_config = {
"cwd": workspace_root,
"memory_manager": memory_manager
} if memory_manager else {"cwd": workspace_root}
for tool_name in tool_manager.tool_classes.keys():
try:
tool = tool_manager.create_tool(tool_name)
if tool:
if tool_name in ['read', 'write', 'edit', 'bash', 'grep', 'find', 'ls']:
tool.config = file_config
tool.cwd = file_config.get("cwd", tool.cwd if hasattr(tool, 'cwd') else None)
if 'memory_manager' in file_config:
tool.memory_manager = file_config['memory_manager']
elif tool_name == 'bocha_search':
bocha_api_key = conf().get("bocha_api_key", "")
if bocha_api_key:
tool.config = {"bocha_api_key": bocha_api_key}
tool.api_key = bocha_api_key
tools.append(tool)
except Exception as e:
logger.warning(f"[AgentBridge] Failed to load tool {tool_name} for session {session_id}: {e}")
if memory_tools:
tools.extend(memory_tools)
# Load context files
context_files = load_context_files(workspace_root)
# 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)
# Build system prompt
prompt_builder = PromptBuilder(
workspace_dir=workspace_root,
language="zh"
)
runtime_info = {
"model": conf().get("model", "unknown"),
"workspace": workspace_root,
"channel": conf().get("channel_type", "unknown")
}
system_prompt = prompt_builder.build(
tools=tools,
context_files=context_files,
memory_manager=memory_manager,
runtime_info=runtime_info,
is_first_conversation=is_first
)
if is_first:
mark_conversation_started(workspace_root)
# Create agent for this session
agent = self.create_agent(
system_prompt=system_prompt,
tools=tools,
max_steps=50,
output_mode="logger",
workspace_dir=workspace_root,
enable_skills=True
)
if memory_manager:
agent.memory_manager = memory_manager
# Store agent for this session
self.agents[session_id] = agent
logger.info(f"[AgentBridge] Agent created for session: {session_id}")
def agent_reply(self, query: str, context: Context = None,
on_event=None, clear_history: bool = False) -> Reply:
@@ -378,7 +524,7 @@ class AgentBridge:
Args:
query: User query
context: COW context (optional)
context: COW context (optional, contains session_id for user isolation)
on_event: Event callback (optional)
clear_history: Whether to clear conversation history
@@ -386,8 +532,13 @@ class AgentBridge:
Reply object
"""
try:
# Get agent (will auto-initialize if needed)
agent = self.get_agent()
# Extract session_id from context for user isolation
session_id = None
if context:
session_id = context.kwargs.get("session_id") or context.get("session_id")
# Get agent for this session (will auto-initialize if needed)
agent = self.get_agent(session_id=session_id)
if not agent:
return Reply(ReplyType.ERROR, "Failed to initialize super agent")
@@ -402,4 +553,21 @@ class AgentBridge:
except Exception as e:
logger.error(f"Agent reply error: {e}")
return Reply(ReplyType.ERROR, f"Agent error: {str(e)}")
return Reply(ReplyType.ERROR, f"Agent error: {str(e)}")
def clear_session(self, session_id: str):
"""
Clear a specific session's agent and conversation history
Args:
session_id: Session identifier to clear
"""
if session_id in self.agents:
logger.info(f"[AgentBridge] Clearing session: {session_id}")
del self.agents[session_id]
def clear_all_sessions(self):
"""Clear all agent sessions"""
logger.info(f"[AgentBridge] Clearing all sessions ({len(self.agents)} total)")
self.agents.clear()
self.default_agent = None

View File

@@ -49,8 +49,6 @@ class WebChannel(ChatChannel):
self.msg_id_counter = 0 # 添加消息ID计数器
self.session_queues = {} # 存储session_id到队列的映射
self.request_to_session = {} # 存储request_id到session_id的映射
# web channel无需前缀
conf()["single_chat_prefix"] = [""]
def _generate_msg_id(self):
@@ -122,18 +120,30 @@ class WebChannel(ChatChannel):
if session_id not in self.session_queues:
self.session_queues[session_id] = Queue()
# Web channel 不需要前缀,确保消息能通过前缀检查
trigger_prefixs = conf().get("single_chat_prefix", [""])
if check_prefix(prompt, trigger_prefixs) is None:
# 如果没有匹配到前缀,给消息加上第一个前缀
if trigger_prefixs:
prompt = trigger_prefixs[0] + prompt
logger.debug(f"[WebChannel] Added prefix to message: {prompt}")
# 创建消息对象
msg = WebMessage(self._generate_msg_id(), prompt)
msg.from_user_id = session_id # 使用会话ID作为用户ID
# 创建上下文
context = self._compose_context(ContextType.TEXT, prompt, msg=msg)
# 创建上下文,明确指定 isgroup=False
context = self._compose_context(ContextType.TEXT, prompt, msg=msg, isgroup=False)
# 检查 context 是否为 None可能被插件过滤等
if context is None:
logger.warning(f"[WebChannel] Context is None for session {session_id}, message may be filtered")
return json.dumps({"status": "error", "message": "Message was filtered"})
# 添加必要的字段
# 覆盖必要的字段_compose_context 会设置默认值,但我们需要使用实际的 session_id
context["session_id"] = session_id
context["receiver"] = session_id
context["request_id"] = request_id
context["isgroup"] = False # 添加 isgroup 字段
context["receiver"] = session_id # 添加 receiver 字段
# 异步处理消息 - 只传递上下文
threading.Thread(target=self.produce, args=(context,)).start()

View File

@@ -185,7 +185,8 @@ available_setting = {
"Minimax_base_url": "",
"web_port": 9899,
"agent": False, # 是否开启Agent模式
"agent_workspace": "~/cow" # agent工作空间路径用于存储skills、memory等
"agent_workspace": "~/cow", # agent工作空间路径用于存储skills、memory等
"bocha_api_key": ""
}

View File

@@ -68,10 +68,11 @@ skill-name/
- Must test scripts before including
**references/** - When to include:
- Documentation for agent to reference
- Database schemas, API docs, domain knowledge
- **ONLY** when documentation is too large for SKILL.md (>500 lines)
- Database schemas, complex API specs that agent needs to reference
- Agent reads these files into context as needed
- For large files (>10k words), include grep patterns in SKILL.md
- **NOT for**: API reference docs, usage examples, tutorials (put in SKILL.md instead)
- **Rule of thumb**: If it fits in SKILL.md, don't create a separate reference file
**assets/** - When to include:
- Files used in output (not loaded to context)
@@ -82,11 +83,15 @@ skill-name/
### What NOT to Include
Do NOT create auxiliary documentation:
- README.md
- INSTALLATION_GUIDE.md
- CHANGELOG.md
- Other non-essential files
Do NOT create auxiliary documentation files:
- README.md - Instructions belong in SKILL.md
- INSTALLATION_GUIDE.md - Setup info belongs in SKILL.md
- CHANGELOG.md - Not needed for local skills
- API_REFERENCE.md - Put API docs directly in SKILL.md
- USAGE_EXAMPLES.md - Put examples directly in SKILL.md
- Any other documentation files - Everything goes in SKILL.md unless it's too large
**Critical Rule**: Only create files that the agent will actually execute (scripts) or that are too large for SKILL.md (references). Documentation, examples, and guides ALL belong in SKILL.md.
## Skill Creation Process
@@ -133,22 +138,31 @@ To turn concrete examples into an effective skill, analyze each example by:
1. Considering how to execute on the example from scratch
2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly
**Planning Checklist**:
-**Always needed**: SKILL.md with clear description and usage instructions
-**scripts/**: Only if code needs to be executed (not just shown as examples)
-**references/**: Rarely needed - only if documentation is >500 lines and can't fit in SKILL.md
-**assets/**: Only if files are used in output (templates, boilerplate, etc.)
Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows:
1. Rotating a PDF requires re-writing the same code each time
2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill
3. ❌ Don't create `references/api-docs.md` - put API info in SKILL.md instead
Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows:
1. Writing a frontend webapp requires the same boilerplate HTML/React each time
2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill
3. ❌ Don't create `references/usage-examples.md` - put examples in SKILL.md instead
Example: When building a `big-query` skill to handle queries like "How many users have logged in today?" the analysis shows:
1. Querying BigQuery requires re-discovering the table schemas and relationships each time
2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill
2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill (ONLY because schemas are very large)
3. ❌ Don't create separate `references/query-examples.md` - put examples in SKILL.md instead
To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets.
To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets. **Default to putting everything in SKILL.md unless there's a compelling reason to separate it.**
### Step 3: Initialize the Skill
@@ -200,6 +214,12 @@ These files contain established best practices for effective skill design.
To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`.
**Important Guidelines**:
- **scripts/**: Only create scripts that will be executed. Test all scripts before including.
- **references/**: ONLY create if documentation is too large for SKILL.md (>500 lines). Most skills don't need this.
- **assets/**: Only include files used in output (templates, icons, etc.)
- **Default approach**: Put everything in SKILL.md unless there's a specific reason not to.
Added scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion.
If you used `--examples`, delete any placeholder files that are not needed for the skill. Only create resource directories that are actually required.