The specification for age is here. As explained in the X25519 recipient stanza section, an ephemeral key pair is generated, and the ephemeral private key is used to perform an X25519 key exchange with the recipient public key. This computes a shared secret that's used to derive a key using HKDF-SHA-256. Then the ephemeral public key is included in the encrypted file to allow the recipient to compute the same shared secret using their private key, enabling decryption.
ephemeral secret = read(CSPRNG, 32)
ephemeral share = X25519(ephemeral secret, basepoint)
salt = ephemeral share || recipient
info = "age-encryption.org/v1/X25519"
shared secret = X25519(ephemeral secret, recipient)
wrap key = HKDF-SHA-256(ikm = shared secret, salt, info)
body = ChaCha20-Poly1305(key = wrap key, plaintext = file key)
The downside of this approach is that it provides no sender authentication. It's like libsodium's sealed box in that the sender's identity is anonymous. In the context of age, this can be considered a design flaw because if you receive a file from someone, you want to know that it came from them and was not replaced by an attacker.
The best further reading on this topic is a series of blog posts by Neil Madden on public key authenticated encryption. Part 2, which I've linked above, explains what age is doing and better alternatives.
Additionally, I would strongly recommend Real-World Cryptography by David Wong, which is by far the best introductory book I've come across and has some facts/stories even more experienced people may not know. It covers Curve25519/X25519 and hybrid encryption with ECIES like age is doing.
Another important thing to read is the Curve25519/X25519 RFC, particularly the Security Considerations section, which has an important detail that's often overlooked. There's the original paper. Finally, there are various blog posts like this and this.