package flyto import ( "errors" "fmt" "strings" ) // errors.go - 结构化错误类型. // // 消费者可通过 errors.As 提取 *EngineError 获取错误码和恢复建议, // 无需解析错误字符串. // ErrorCode 是引擎错误码枚举. type ErrorCode string const ( ErrAPIAuth ErrorCode = "api_auth_error" ErrAPIRateLimit ErrorCode = "api_rate_limit" ErrAPIOverloaded ErrorCode = "api_overloaded" ErrAPIBadRequest ErrorCode = "api_bad_request" ErrToolNotFound ErrorCode = "tool_not_found" ErrToolExecution ErrorCode = "tool_execution_error" ErrPermissionDenied ErrorCode = "permission_denied" ErrContextOverflow ErrorCode = "context_overflow" ErrBudgetExceeded ErrorCode = "budget_exceeded" ErrMaxTurns ErrorCode = "max_turns_reached" ErrSessionNotFound ErrorCode = "session_not_found" ErrSessionClosed ErrorCode = "session_closed" ErrMCPConnection ErrorCode = "mcp_connection_error" ErrPluginLoad ErrorCode = "plugin_load_error" ErrInternal ErrorCode = "internal_error" ErrStreamTruncated ErrorCode = "stream_truncated" // ADR-0006 typed error codes (Bug U fail-loud audit, 2026-05-01). // Mirror of engine/errors.go constants -- duplicated here so wire // layer (which only imports flyto) can construct typed errors that // engine layer recognizes via errors.As. ADR-0006 § 3 forbids using // these to drive auto-fallback inside the engine; classify only, // recovery decisions belong to the consumer. // // 与 engine/errors.go 同步 (Bug U fail-loud audit, 2026-05-01). // wire 层只 import flyto 不 import engine; 在此声明让 wire 构造 // typed error, engine 层通过 errors.As 识别. ADR-0006 § 3 红线: 这些 // code 只供分类, 引擎层禁用驱动自动 fallback, 恢复决策归消费方. ErrProviderHTTPStatus ErrorCode = "provider_http_status" ErrProviderNonSSE ErrorCode = "provider_non_sse" ErrProviderMidStreamErr ErrorCode = "provider_mid_stream_err" ErrWireUnmarshal ErrorCode = "wire_unmarshal_error" ErrModelToolUnsupported ErrorCode = "model_tool_unsupported" // ADR-0008 v2 reflector-vs-tool boundary. Returned by engine.New // when cfg.ResponseReflector.Name() collides with a Tool.Name() in // cfg.Toolset. Reflectors are engine-forced hooks (LLM-invisible, // fail-closed retry); double-wiring lets the LLM call the same // logic as an ordinary tool, blurring reflector-vs-tool semantics // that ADR-0008 v2 established. Mirror of engine/errors.go for // the same fail-loud reason -- consumer can detect via errors.As. // // ADR-0008 v2 反射器 vs 工具边界. engine.New 在 cfg.ResponseReflector. // Name() 与 cfg.Toolset 里某个 Tool.Name() 撞名时返回. 反射器是引擎 // 强制 hook (LLM 不感知, fail-closed retry); 双挂让 LLM 把同一逻辑 // 当普通工具调, 模糊 ADR-0008 v2 立的反射器 vs 工具语义. 与 // engine/errors.go 镜像同款 fail-loud 理由 -- 消费方经 errors.As 识别. ErrReflectorDoubleWired ErrorCode = "reflector_double_wired" ) // EngineError 是引擎统一错误类型. type EngineError struct { Code ErrorCode Message string Detail string Suggestion string Cause error Retryable bool } // Error implements the error interface. When Detail is populated the // canonical Go wrapping form `Message: Detail` is used so any // errors.Error() / fmt.Errorf("%v") consumer sees the wire-layer // specifics without type-asserting. Mirror of engine/errors.go for // the same fail-loud reason. // // Error 实现 error 接口. Detail 非空时返 "Message: Detail" 让任何走 // errors.Error() / fmt.Errorf("%v") 消费方不必 type-assert 看到 wire // 层细节. 与 engine/errors.go 镜像同款 fail-loud 理由. func (e *EngineError) Error() string { switch { case e.Message != "" && e.Detail != "": return e.Message + ": " + e.Detail case e.Message != "": return e.Message case e.Detail != "": return e.Detail default: return string(e.Code) } } func (e *EngineError) Unwrap() error { return e.Cause } var defaultSuggestions = map[ErrorCode]string{ ErrAPIAuth: "请检查 API Key 环境变量是否正确设置", ErrAPIRateLimit: "已达到 API 速率限制,将自动重试", ErrAPIOverloaded: "API 服务暂时过载,将自动重试", ErrAPIBadRequest: "请求格式错误,请检查输入参数", ErrToolNotFound: "请检查工具名称是否正确", ErrToolExecution: "工具执行失败,请检查输入参数和工作目录", ErrPermissionDenied: "权限被拒绝,可通过修改权限模式或添加规则来授权", ErrContextOverflow: "对话上下文已满,已自动压缩", ErrBudgetExceeded: "已达到预算上限", ErrMaxTurns: "已达到最大对话轮次限制", ErrSessionNotFound: "指定的会话不存在", ErrSessionClosed: "会话已关闭,请创建新的会话", ErrMCPConnection: "MCP 服务器连接失败,请检查服务器配置", ErrPluginLoad: "插件加载失败,请检查插件配置", ErrInternal: "发生内部错误,请重试", ErrStreamTruncated: "流式响应被中间代理截断,请检查网络代理配置", // ADR-0006 typed errors -- actionable hint, 不是 auto-action. 见 // 顶部红线注释. ErrProviderHTTPStatus: "上游 provider 返回非 2xx HTTP, 检查 provider 状态或切换", ErrProviderNonSSE: "上游返回非 SSE 响应体 (配置错或代理拦截), 查看 Detail", ErrProviderMidStreamErr: "上游 SSE 流中部错误事件, 查看 Detail provider 错误码与消息", ErrWireUnmarshal: "wire 层无法解析 SSE chunk (provider 输出畸形或协议变更)", ErrModelToolUnsupported: "底层 provider 拒绝 tool schema (常见: 名违反 ^[a-zA-Z0-9_-]+$ / 数量超限 / schema 不支持字段)", } var defaultRetryable = map[ErrorCode]bool{ ErrAPIRateLimit: true, ErrAPIOverloaded: true, ErrMCPConnection: true, ErrInternal: true, ErrStreamTruncated: true, } // NewEngineError 创建引擎错误(使用默认 Suggestion 和 Retryable). func NewEngineError(code ErrorCode, message string, cause error) *EngineError { return &EngineError{ Code: code, Message: message, Suggestion: defaultSuggestions[code], Cause: cause, Retryable: defaultRetryable[code], } } // WrapError 将普通错误包装为 EngineError. func WrapError(cause error, code ErrorCode, message string) *EngineError { if cause == nil { return NewEngineError(code, message, nil) } var existing *EngineError if errors.As(cause, &existing) { return &EngineError{ Code: code, Message: message, Detail: existing.Detail, Suggestion: defaultSuggestions[code], Cause: cause, Retryable: defaultRetryable[code], } } return &EngineError{ Code: code, Message: message, Detail: cause.Error(), Suggestion: defaultSuggestions[code], Cause: cause, Retryable: defaultRetryable[code], } } // IsRetryable 检查错误是否可重试. func IsRetryable(err error) bool { if err == nil { return false } var e *EngineError if errors.As(err, &e) { return e.Retryable } s := err.Error() return strings.Contains(s, "HTTP 429") || strings.Contains(s, "HTTP 529") || strings.Contains(s, "connection reset") || strings.Contains(s, "timeout") } // FormatErrorForDisplay 格式化错误供用户阅读. func FormatErrorForDisplay(err error, verbose bool) string { var e *EngineError if !errors.As(err, &e) { return fmt.Sprintf("错误: %s", err.Error()) } var sb strings.Builder sb.WriteString(fmt.Sprintf("错误: %s", e.Message)) if e.Suggestion != "" { sb.WriteString(fmt.Sprintf("\n建议: %s", e.Suggestion)) } if verbose && e.Detail != "" { sb.WriteString(fmt.Sprintf("\n详情: %s", e.Detail)) } if e.Retryable { sb.WriteString("\n(此错误可自动重试)") } return sb.String() }