You can make use of an ARRAY type internally. Argument type can still be any numeric type. Demonstrating with float (= double precision):
CREATE OR REPLACE FUNCTION f_circavg (float[], float)
  RETURNS float[] LANGUAGE sql STRICT AS
'SELECT ARRAY[$1[1] + sin($2), $1[2] + cos($2), 1]';
CREATE OR REPLACE FUNCTION f_circavg_final (float[])
  RETURNS float  LANGUAGE sql AS
'SELECT CASE WHEN $1[3] > 0 THEN atan2($1[1], $1[2]) END';
CREATE AGGREGATE circavg (float) (
   sfunc     = f_circavg
 , stype     = float[]
 , finalfunc = f_circavg_final
 , initcond  = '{0,0,0}'
);
The transition function f_circavg() is defined STRICT, so it ignores rows with NULL input. It also sets a third array element to identify sets with one or more input rows - else the CASE the final function returns NULL.
Temporary table for testing:
CREATE TEMP TABLE t (x float);
INSERT INTO t VALUES (2), (NULL), (3), (4), (5);
I threw in a NULL value to also test the STRICT magic. Call:
SELECT circavg(x) FROM t;
       circavg
-------------------
 -2.78318530717959
Cross check:
SELECT atan2(sum(sin(x)), sum(cos(x))) FROM t;
       atan2
-------------------
 -2.78318530717959
Returns the same. Seems to work. In test with a bigger table the last expression with regular aggregate functions was 4x faster than the custom aggregate.
Test for zero input rows / only NULL input:
SELECT circavg(x) FROM t WHERE false;     -- no input rows
SELECT circavg(x) FROM t WHERE x IS NULL; -- only NULL input
Returns NULL in both cases.