mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-09 20:54:31 +02:00
Add files via upload
This commit is contained in:
@@ -230,54 +230,61 @@ attemptLoop:
|
||||
continue
|
||||
}
|
||||
if ev.Err != nil {
|
||||
canRetry := attempt+1 < maxToolCallRecoveryAttempts
|
||||
|
||||
if canRetry && isRecoverableToolCallArgumentsJSONError(ev.Err) {
|
||||
if logger != nil {
|
||||
logger.Warn("eino: recoverable tool-call JSON error from model/API", zap.Error(ev.Err), zap.Int("attempt", attempt))
|
||||
}
|
||||
retryHints = append(retryHints, toolCallArgumentsJSONRetryHint())
|
||||
if progress != nil {
|
||||
progress("eino_recovery", toolCallArgumentsJSONRecoveryTimelineMessage(attempt), map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
"einoRetry": attempt,
|
||||
"runIndex": attempt + 1,
|
||||
"maxRuns": maxToolCallRecoveryAttempts,
|
||||
"reason": "invalid_tool_arguments_json",
|
||||
})
|
||||
}
|
||||
continue attemptLoop
|
||||
}
|
||||
|
||||
if canRetry && isRecoverableToolExecutionError(ev.Err) {
|
||||
if logger != nil {
|
||||
logger.Warn("eino: recoverable tool execution error, will retry with corrective hint",
|
||||
zap.Error(ev.Err), zap.Int("attempt", attempt))
|
||||
}
|
||||
// context.Canceled 是唯一应当直接终止编排的错误(用户关闭页面、主动停止等)。
|
||||
if errors.Is(ev.Err, context.Canceled) {
|
||||
flushAllPendingAsFailed(ev.Err)
|
||||
retryHints = append(retryHints, toolExecutionRetryHint())
|
||||
if progress != nil {
|
||||
progress("eino_recovery", toolExecutionRecoveryTimelineMessage(attempt), map[string]interface{}{
|
||||
progress("error", ev.Err.Error(), map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
"einoRetry": attempt,
|
||||
"runIndex": attempt + 1,
|
||||
"maxRuns": maxToolCallRecoveryAttempts,
|
||||
"reason": "tool_execution_error",
|
||||
})
|
||||
}
|
||||
continue attemptLoop
|
||||
return nil, ev.Err
|
||||
}
|
||||
|
||||
canRetry := attempt+1 < maxToolCallRecoveryAttempts
|
||||
if !canRetry {
|
||||
// 重试次数已耗尽,终止。
|
||||
flushAllPendingAsFailed(ev.Err)
|
||||
if progress != nil {
|
||||
progress("error", ev.Err.Error(), map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
})
|
||||
}
|
||||
return nil, ev.Err
|
||||
}
|
||||
|
||||
// 区分错误类型以选择最合适的纠错提示,但无论哪种都执行重试(default-soft)。
|
||||
var hint *schema.Message
|
||||
var reason, timelineMsg string
|
||||
if isRecoverableToolCallArgumentsJSONError(ev.Err) {
|
||||
hint = toolCallArgumentsJSONRetryHint()
|
||||
reason = "invalid_tool_arguments_json"
|
||||
timelineMsg = toolCallArgumentsJSONRecoveryTimelineMessage(attempt)
|
||||
} else {
|
||||
hint = toolExecutionRetryHint()
|
||||
reason = "tool_execution_error"
|
||||
timelineMsg = toolExecutionRecoveryTimelineMessage(attempt)
|
||||
}
|
||||
|
||||
if logger != nil {
|
||||
logger.Warn("eino: recoverable error, will retry with corrective hint",
|
||||
zap.Error(ev.Err), zap.Int("attempt", attempt), zap.String("reason", reason))
|
||||
}
|
||||
flushAllPendingAsFailed(ev.Err)
|
||||
retryHints = append(retryHints, hint)
|
||||
if progress != nil {
|
||||
progress("error", ev.Err.Error(), map[string]interface{}{
|
||||
progress("eino_recovery", timelineMsg, map[string]interface{}{
|
||||
"conversationId": conversationID,
|
||||
"source": "eino",
|
||||
"einoRetry": attempt,
|
||||
"runIndex": attempt + 1,
|
||||
"maxRuns": maxToolCallRecoveryAttempts,
|
||||
"reason": reason,
|
||||
})
|
||||
}
|
||||
return nil, ev.Err
|
||||
continue attemptLoop
|
||||
}
|
||||
if ev.AgentName != "" && progress != nil {
|
||||
iterEinoAgent := orchestratorName
|
||||
|
||||
@@ -41,62 +41,27 @@ func softRecoveryToolCallMiddleware() compose.InvokableToolMiddleware {
|
||||
|
||||
// isSoftRecoverableToolError determines whether a tool execution error should be
|
||||
// silently converted to a tool-result message rather than crashing the graph.
|
||||
//
|
||||
// Design: default-soft (blacklist). Almost every tool execution error should be
|
||||
// fed back to the LLM so it can self-correct or choose an alternative tool.
|
||||
// Only a small set of "truly fatal" conditions (user cancellation) should
|
||||
// propagate as hard errors that terminate the orchestration graph.
|
||||
// This avoids the fragile whitelist approach where every new error pattern
|
||||
// would need to be explicitly enumerated.
|
||||
func isSoftRecoverableToolError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 用户取消 — 不应重试,让 hard error 传播以终止编排。
|
||||
// 用户主动取消 — 唯一应当终止编排的情况,不应重试。
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 工具执行超时 — 转为 soft error 让 LLM 知晓并选择替代方案,而非全局重试。
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return true
|
||||
}
|
||||
|
||||
s := strings.ToLower(err.Error())
|
||||
|
||||
// JSON unmarshal/parse failures — the model generated truncated or malformed arguments.
|
||||
if isJSONRelatedError(s) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Sub-agent type not found (from deep/task_tool.go)
|
||||
if strings.Contains(s, "subagent type") && strings.Contains(s, "not found") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Tool not found in ToolsNode indexes
|
||||
if strings.Contains(s, "tool") && strings.Contains(s, "not found") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isJSONRelatedError checks whether an error string indicates a JSON parsing problem.
|
||||
func isJSONRelatedError(lower string) bool {
|
||||
if !strings.Contains(lower, "json") {
|
||||
return false
|
||||
}
|
||||
jsonIndicators := []string{
|
||||
"unexpected end of json",
|
||||
"unmarshal",
|
||||
"invalid character",
|
||||
"cannot unmarshal",
|
||||
"invalid tool arguments",
|
||||
"failed to unmarshal",
|
||||
"must be in json format",
|
||||
"unexpected eof",
|
||||
}
|
||||
for _, ind := range jsonIndicators {
|
||||
if strings.Contains(lower, ind) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
// 其他所有工具执行错误(超时、命令不存在、JSON 解析失败、工具未找到、
|
||||
// 权限不足、网络不可达……)一律转为 soft error,让 LLM 看到错误信息
|
||||
// 后自行决策:换工具、调整参数、或向用户说明。
|
||||
return true
|
||||
}
|
||||
|
||||
// buildSoftRecoveryMessage creates a bilingual error message that the LLM can act on.
|
||||
|
||||
@@ -53,7 +53,12 @@ func TestIsSoftRecoverableToolError(t *testing.T) {
|
||||
{
|
||||
name: "unrelated network error",
|
||||
err: errors.New("connection refused"),
|
||||
expected: false,
|
||||
expected: true, // default-soft: non-cancel errors are recoverable
|
||||
},
|
||||
{
|
||||
name: "tool binary not installed",
|
||||
err: errors.New("[LocalFunc] failed to invoke tool, toolName=grep, err=ripgrep (rg) is not installed or not in PATH"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "context cancelled",
|
||||
@@ -131,15 +136,16 @@ func TestSoftRecoveryToolCallMiddleware_PropagatesNonRecoverable(t *testing.T) {
|
||||
return nil, origErr
|
||||
}
|
||||
wrapped := mw(next)
|
||||
_, err := wrapped(context.Background(), &compose.ToolInput{
|
||||
out, err := wrapped(context.Background(), &compose.ToolInput{
|
||||
Name: "test_tool",
|
||||
Arguments: `{}`,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error to propagate for non-recoverable errors")
|
||||
// Default-soft: non-cancel errors are converted to tool-result messages.
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error (soft recovery), got: %v", err)
|
||||
}
|
||||
if err != origErr {
|
||||
t.Fatalf("expected original error, got: %v", err)
|
||||
if out == nil || out.Result == "" {
|
||||
t.Fatal("expected non-empty recovery message")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,74 +2,42 @@ package multiagent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// isRecoverableToolExecutionError detects tool-level execution errors that can be
|
||||
// recovered by retrying with a corrective hint. These errors originate from eino
|
||||
// framework internals (e.g. task_tool.go, tool_node.go) when the LLM produces
|
||||
// invalid tool calls such as non-existent sub-agent types, malformed JSON arguments,
|
||||
// or unregistered tool names.
|
||||
func isRecoverableToolExecutionError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
s := strings.ToLower(err.Error())
|
||||
|
||||
// Sub-agent type not found (from deep/task_tool.go)
|
||||
if strings.Contains(s, "subagent type") && strings.Contains(s, "not found") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Tool not found in toolsNode indexes (from compose/tool_node.go, when UnknownToolsHandler is nil)
|
||||
if strings.Contains(s, "tool") && strings.Contains(s, "not found") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Invalid tool arguments JSON (from einomcp/mcp_tools.go or eino internals)
|
||||
if strings.Contains(s, "invalid tool arguments json") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Failed to unmarshal task tool input json (from deep/task_tool.go)
|
||||
if strings.Contains(s, "failed to unmarshal") && strings.Contains(s, "json") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Generic tool call stream/invoke failure wrapping the above
|
||||
if (strings.Contains(s, "failed to stream tool call") || strings.Contains(s, "failed to invoke tool")) &&
|
||||
(strings.Contains(s, "not found") || strings.Contains(s, "json") || strings.Contains(s, "unmarshal")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// toolExecutionRetryHint returns a user message appended to the conversation to prompt
|
||||
// the LLM to correct its tool call after a tool execution error.
|
||||
// the LLM to adjust after a tool execution error (tool not found, binary missing,
|
||||
// runtime failure, network error, etc.).
|
||||
func toolExecutionRetryHint() *schema.Message {
|
||||
return schema.UserMessage(`[System] Your previous tool call failed because:
|
||||
- The tool or sub-agent name you used does not exist, OR
|
||||
return schema.UserMessage(`[System] Your previous tool call failed. Possible causes:
|
||||
- The tool or sub-agent name does not exist (typo or unregistered name).
|
||||
- The tool call arguments were not valid JSON.
|
||||
- The tool's underlying binary is not installed or not in PATH.
|
||||
- The tool encountered a runtime error (timeout, network failure, permission denied, etc.).
|
||||
|
||||
Please carefully review the available tools and sub-agents listed in your context, use only exact registered names (case-sensitive), and ensure all arguments are well-formed JSON objects. Then retry your action.
|
||||
Please review the error message above, check available tools, and either:
|
||||
1. Retry with corrected arguments or a different tool, OR
|
||||
2. Inform the user about the limitation and proceed with an alternative approach.
|
||||
|
||||
[系统提示] 上一次工具调用失败,可能原因:
|
||||
- 你使用的工具名或子代理名称不存在;
|
||||
- 工具调用参数不是合法 JSON。
|
||||
- 工具名或子代理名称不存在(拼写错误或未注册);
|
||||
- 工具调用参数不是合法 JSON;
|
||||
- 工具依赖的底层二进制程序未安装或不在 PATH 中;
|
||||
- 工具运行时遇到错误(超时、网络故障、权限不足等)。
|
||||
|
||||
请仔细检查上下文中列出的可用工具和子代理名称(须完全匹配、区分大小写),确保所有参数均为合法的 JSON 对象,然后重新执行。`)
|
||||
请根据上述错误信息检查可用工具,然后:
|
||||
1. 修正参数或改用其他工具重试,或者
|
||||
2. 告知用户当前限制并采用替代方案继续。`)
|
||||
}
|
||||
|
||||
// toolExecutionRecoveryTimelineMessage returns a message for the eino_recovery event
|
||||
// displayed in the UI timeline when a tool execution error triggers a retry.
|
||||
func toolExecutionRecoveryTimelineMessage(attempt int) string {
|
||||
return fmt.Sprintf(
|
||||
"工具调用执行失败(工具/子代理名称不存在或参数 JSON 无效)。已向对话追加纠错提示并要求模型重新生成。"+
|
||||
"工具调用执行失败。已向对话追加纠错提示并要求模型调整策略。"+
|
||||
"当前为第 %d/%d 轮完整运行。\n\n"+
|
||||
"Tool call execution failed (unknown tool/sub-agent name or invalid JSON arguments). "+
|
||||
"Tool call execution failed. "+
|
||||
"A corrective hint was appended. This is full run %d of %d.",
|
||||
attempt+1, maxToolCallRecoveryAttempts, attempt+1, maxToolCallRecoveryAttempts,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user