Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

266 lines
9.2 KiB
Go

package agentruntime
import (
"encoding/json"
"regexp"
"strconv"
"strings"
)
const (
providerFailureOriginProvider = "provider"
providerFailureOriginTransport = "transport"
providerFailureAuthNone = "none"
providerFailureAuthRequired = "required"
)
// ProviderFailure is the normalized boundary between provider protocols and
// canonical activity. Provider adapters own classification; shared layers must
// not infer authentication state from provider prose.
type ProviderFailure struct {
Code string
ProviderCode string
HTTPStatus *int
Message string
AdditionalDetails string
Retryable *bool
Origin string
AuthImpact string
AuthReason string
}
func (failure ProviderFailure) metadata() map[string]any {
message := sanitizeProviderFailureText(failure.Message)
details := sanitizeProviderFailureText(failure.AdditionalDetails)
payload := map[string]any{
"code": firstNonEmptyString(strings.TrimSpace(failure.Code), "provider_error"),
"origin": firstNonEmptyString(strings.TrimSpace(failure.Origin), providerFailureOriginProvider),
"authImpact": firstNonEmptyString(strings.TrimSpace(failure.AuthImpact), providerFailureAuthNone),
}
if message != "" {
payload["error"] = message
payload["errorMessage"] = message
}
if details != "" && details != message {
payload["additionalDetails"] = details
}
if code := strings.TrimSpace(failure.ProviderCode); code != "" {
payload["providerCode"] = code
}
if failure.HTTPStatus != nil {
payload["httpStatus"] = *failure.HTTPStatus
}
if failure.Retryable != nil {
payload["retryable"] = *failure.Retryable
}
if failure.AuthImpact == providerFailureAuthRequired {
payload["authReason"] = firstNonEmptyString(strings.TrimSpace(failure.AuthReason), "auth_required")
}
return payload
}
func claudeProviderFailure(payload map[string]any) ProviderFailure {
providerCode := strings.TrimSpace(payloadString(payload, "code"))
message := strings.TrimSpace(payloadString(payload, "error"))
statusValue := payloadInt64(payload, "apiErrorStatus")
var status *int
if statusValue > 0 {
value := int(statusValue)
status = &value
}
failure := ProviderFailure{
Code: "provider_error",
ProviderCode: providerCode,
HTTPStatus: status,
Message: firstNonEmptyString(message, providerCode, "Claude provider rejected the Turn before acceptance"),
Origin: providerFailureOriginProvider,
AuthImpact: providerFailureAuthNone,
}
switch providerCode {
case "authentication_failed":
// Claude's SDK also uses this code for some HTTP 403 account failures;
// only a real 401 (or a missing status) is an authentication gate.
if claudeProviderInsufficientAccountBalance(message) {
failure.Code = FailureCodeInsufficientCredits
} else if status == nil || *status == 401 {
failure.Code, failure.AuthImpact, failure.AuthReason = "auth_required", providerFailureAuthRequired, "authentication_failed"
}
case "oauth_org_not_allowed":
failure.Code = "account_not_allowed"
case "billing_error":
failure.Code = "billing_error"
case "rate_limit":
failure.Code = FailureCodeQuotaOrRateLimit
case "overloaded", "server_error":
failure.Code = "provider_unavailable"
case "invalid_request":
failure.Code = "invalid_request"
case "model_not_found":
failure.Code = "model_not_available"
case "max_output_tokens":
failure.Code = "max_output_tokens"
case "unknown":
if status == nil {
failure.Code = "network_error"
failure.Origin = providerFailureOriginTransport
}
}
if status != nil {
switch *status {
case 401:
if providerCode == "" || providerCode == "unknown" || providerCode == "authentication_failed" {
failure.Code, failure.AuthImpact, failure.AuthReason = "auth_required", providerFailureAuthRequired, "http_401"
}
case 408, 522, 524:
failure.Code = "request_timed_out"
default:
if *status >= 500 && failure.Code == "provider_error" {
failure.Code = "provider_unavailable"
}
}
}
return failure
}
func claudeProviderInsufficientAccountBalance(message string) bool {
return strings.Contains(strings.ToLower(message), "insufficient account balance")
}
func failureFromACPCall(err *acpCallError) ProviderFailure {
failure := ProviderFailure{
Code: "provider_error",
Message: err.Err.Message,
Origin: providerFailureOriginProvider,
AuthImpact: providerFailureAuthNone,
}
data := acpErrorDataPayload(err.Err.Data)
if message := strings.TrimSpace(asString(data["message"])); message != "" {
failure.Message = message
}
if details := firstNonEmpty(asString(data["additional_details"]), asString(data["additionalDetails"])); details != "" {
failure.AdditionalDetails = details
}
info := firstPresentAny(data["codex_error_info"], data["codexErrorInfo"])
providerCode, httpStatus := codexFailureIdentity(info)
if providerCode != "" {
failure.ProviderCode = providerCode
failure.HTTPStatus = httpStatus
failure.Code, failure.AuthImpact, failure.AuthReason = codexFailureClassification(providerCode, httpStatus)
}
if retryable, ok := firstPresentAny(data["will_retry"], data["willRetry"]).(bool); ok {
failure.Retryable = &retryable
}
failure.Message = firstNonEmptyString(failure.Message, failure.AdditionalDetails, failure.ProviderCode, "Provider request failed")
return failure
}
func failureFromCodexTurnError(turnError map[string]any) ProviderFailure {
providerCode, httpStatus := codexFailureIdentity(turnError["codexErrorInfo"])
code, authImpact, authReason := codexFailureClassification(providerCode, httpStatus)
message := asStringRaw(turnError["message"])
details := asStringRaw(turnError["additionalDetails"])
failure := ProviderFailure{
Code: code,
ProviderCode: providerCode,
HTTPStatus: httpStatus,
Message: firstNonEmptyString(message, details, providerCode, "Codex provider request failed"),
AdditionalDetails: details,
Origin: providerFailureOriginProvider,
AuthImpact: authImpact,
AuthReason: authReason,
}
if retryable, ok := turnError["willRetry"].(bool); ok {
failure.Retryable = &retryable
}
return failure
}
func codexFailureIdentity(value any) (string, *int) {
switch typed := value.(type) {
case string:
return strings.TrimSpace(typed), nil
case map[string]any:
code := firstNonEmpty(asString(typed["type"]), asString(typed["code"]), asString(typed["kind"]))
if code == "" && len(typed) == 1 {
for key, nested := range typed {
code = key
if object, ok := nested.(map[string]any); ok {
typed = object
}
}
}
var status *int
for _, candidate := range []any{typed["httpStatusCode"], typed["http_status_code"]} {
if parsed, ok := providerFailureInt(candidate); ok {
status = &parsed
break
}
}
return strings.TrimSpace(code), status
default:
return "", nil
}
}
func codexFailureClassification(providerCode string, httpStatus *int) (string, string, string) {
normalized := strings.ToLower(strings.NewReplacer("_", "", "-", "").Replace(strings.TrimSpace(providerCode)))
switch normalized {
case "unauthorized":
return "auth_required", providerFailureAuthRequired, "unauthorized"
case "usagelimitexceeded", "sessionbudgetexceeded":
return FailureCodeQuotaOrRateLimit, providerFailureAuthNone, ""
case "serveroverloaded", "internalservererror":
return "provider_unavailable", providerFailureAuthNone, ""
case "badrequest", "contextwindowexceeded":
return "invalid_request", providerFailureAuthNone, ""
}
if httpStatus != nil {
switch {
case *httpStatus == 408 || *httpStatus == 522 || *httpStatus == 524:
return "request_timed_out", providerFailureAuthNone, ""
case *httpStatus == 429:
return FailureCodeQuotaOrRateLimit, providerFailureAuthNone, ""
case *httpStatus >= 500:
return "provider_unavailable", providerFailureAuthNone, ""
}
}
switch normalized {
case "httpconnectionfailed", "responsestreamconnectionfailed", "responsestreamdisconnected", "responsetoomanyfailedattempts":
return "provider_stream_disconnected", providerFailureAuthNone, ""
default:
return "provider_error", providerFailureAuthNone, ""
}
}
func providerFailureInt(value any) (int, bool) {
switch typed := value.(type) {
case float64:
return int(typed), true
case int:
return typed, true
case json.Number:
parsed, err := strconv.Atoi(string(typed))
return parsed, err == nil
case string:
parsed, err := strconv.Atoi(strings.TrimSpace(typed))
return parsed, err == nil
default:
return 0, false
}
}
var providerFailureSecretPatterns = []*regexp.Regexp{
regexp.MustCompile(`(?i)("(?:authorization|proxy-authorization|cookie|set-cookie|api[_-]?key|auth[_-]?token|access[_-]?token|refresh[_-]?token|secret|signature)"\s*:\s*")[^"]*`),
regexp.MustCompile(`(?i)(\b(?:cookie|set-cookie)\s*:\s*)[^\r\n]*`),
regexp.MustCompile(`(?i)((?:"?(?:authorization|proxy-authorization|api[_-]?key|auth[_-]?token|access[_-]?token|refresh[_-]?token|secret|signature)"?)\s*[:=]\s*"?(?:bearer\s+)?)[^"\s,;}]+`),
regexp.MustCompile(`(?i)([?&](?:api[_-]?key|key|token|signature)=)[^&\s]+`),
}
func sanitizeProviderFailureText(value string) string {
value = cleanVisibleErrorText(value)
for _, pattern := range providerFailureSecretPatterns {
value = pattern.ReplaceAllString(value, `${1}[REDACTED]`)
}
return limitVisibleErrorDetail(value)
}