diff --git a/tool/functool/func.go b/tool/functool/func.go index 35031eed..c8299a83 100644 --- a/tool/functool/func.go +++ b/tool/functool/func.go @@ -128,7 +128,14 @@ func inputFormatFor[T any]() (format *jsonformat.Format, wrapped bool, err error if typ == reflect.TypeFor[any]() { return nil, false, fmt.Errorf("input type any is not supported by HandlerFor; use Handler for dynamic inputs") } - if typ.Kind() != reflect.Struct { + // Dereference pointers so a *Struct input produces the same flat schema as + // a Struct input (jsonformat.ForType treats pointers equivalently); only + // genuinely non-struct inputs are wrapped in inputWrapper. + elem := typ + for elem.Kind() == reflect.Pointer { + elem = elem.Elem() + } + if elem.Kind() != reflect.Struct { typ = reflect.TypeFor[inputWrapper[T]]() wrapped = true } diff --git a/tool/functool/func_test.go b/tool/functool/func_test.go index c441fda1..a775ba26 100644 --- a/tool/functool/func_test.go +++ b/tool/functool/func_test.go @@ -262,3 +262,24 @@ func TestFuncTool_NewRejectsAnyInput(t *testing.T) { t.Fatalf("unexpected error: %v", err) } } + +// A *Struct input must produce the same flat schema as a Struct input and accept +// flat arguments, not be wrapped in an inputWrapper (which would require {"Arg0":{...}}). +func TestFuncTool_PointerStructInput_UsesFlatSchema(t *testing.T) { + type In struct { + V string `json:"v"` + } + tl := functool.MustNew(functool.Config{Name: "t", Description: "d"}, func(_ context.Context, in *In) (string, error) { + if in == nil { + return "", nil + } + return in.V, nil + }) + ret, err := tl.Call(t.Context(), `{"v":"hi"}`) + if err != nil { + t.Fatalf("Call with flat args failed for *struct input: %v", err) + } + if ret != "hi" { + t.Errorf("ret = %v, want hi", ret) + } +}