package agentruntime import ( "context" "encoding/json" "errors" "fmt" ) const failureSchemaVersion = 1 // FailureCode is the stable, provider-neutral classification of an execution // failure. Provider-specific diagnostics belong in Message or Metadata. type FailureCode string const ( FailureCodeUnknown FailureCode = "unknown" FailureCodeCancelled FailureCode = "cancelled" FailureCodeDeadlineExceeded FailureCode = "deadline_exceeded" FailureCodeInvalidRequest FailureCode = "invalid_request" FailureCodeSessionNotFound FailureCode = "session_not_found" FailureCodeUnavailable FailureCode = "unavailable" FailureCodeQuotaExhausted FailureCode = "quota_exhausted" FailureCodeProcessExit FailureCode = "process_exit" FailureCodeProvider FailureCode = "provider_error" FailureCodeInternal FailureCode = "internal" ) // Failure is the public typed failure value shared by runtime hosts. type Failure struct { Code FailureCode `json:"code"` Message string `json:"message,omitempty"` Retryable bool `json:"retryable"` Metadata map[string]string `json:"metadata,omitempty"` } func (f *Failure) Error() string { if f == nil { return "" } if f.Message != "" { return f.Message } return string(f.Code) } type failureEnvelope struct { SchemaVersion int `json:"schema_version"` Failure *Failure `json:"failure"` } // EncodeFailure serializes a typed failure for durable host boundaries. func EncodeFailure(f *Failure) (string, error) { if f == nil { return "", errors.New("agentruntime: nil failure") } normalized := normalizeFailure(*f) payload, err := json.Marshal(failureEnvelope{ SchemaVersion: failureSchemaVersion, Failure: &normalized, }) if err != nil { return "", fmt.Errorf("agentruntime: encode failure: %w", err) } return string(payload), nil } // DecodeFailure parses a durable failure envelope. Unknown future codes are // preserved in metadata and normalized to FailureCodeUnknown. func DecodeFailure(payload string) (*Failure, error) { var envelope failureEnvelope if err := json.Unmarshal([]byte(payload), &envelope); err != nil { return nil, fmt.Errorf("agentruntime: decode failure: %w", err) } if envelope.SchemaVersion != failureSchemaVersion { return nil, fmt.Errorf("agentruntime: unsupported failure schema version %d", envelope.SchemaVersion) } if envelope.Failure == nil { return nil, errors.New("agentruntime: failure payload is missing") } normalized := normalizeFailure(*envelope.Failure) return &normalized, nil } // FailureFromError maps cancellation boundaries and otherwise returns a // provider-neutral unknown failure without guessing provider policy. func FailureFromError(err error) *Failure { if err == nil { return nil } switch { case errors.Is(err, ErrRunCancelled), errors.Is(err, context.Canceled): return &Failure{Code: FailureCodeCancelled, Message: err.Error()} case errors.Is(err, context.DeadlineExceeded): return &Failure{Code: FailureCodeDeadlineExceeded, Message: err.Error(), Retryable: true} default: return &Failure{Code: FailureCodeUnknown, Message: err.Error()} } } func normalizeFailure(f Failure) Failure { if isKnownFailureCode(f.Code) { return f } metadata := cloneStringMap(f.Metadata) if metadata == nil { metadata = make(map[string]string, 1) } if f.Code != "" { metadata["original_code"] = string(f.Code) } f.Code = FailureCodeUnknown f.Metadata = metadata return f } func isKnownFailureCode(code FailureCode) bool { switch code { case FailureCodeUnknown, FailureCodeCancelled, FailureCodeDeadlineExceeded, FailureCodeInvalidRequest, FailureCodeSessionNotFound, FailureCodeUnavailable, FailureCodeQuotaExhausted, FailureCodeProcessExit, FailureCodeProvider, FailureCodeInternal: return true default: return false } } func cloneStringMap(input map[string]string) map[string]string { if len(input) == 0 { return nil } cloned := make(map[string]string, len(input)) for key, value := range input { cloned[key] = value } return cloned }