You need limited with, which was introduced to address exactly this problem. See the Rationale for Ada 2005, section 4.2.
Animals and Persons are symmetric (my editor has adjusted the layout and casing; I’ve added one record component to each so the demo program, below, can print something):
limited with Animals;
package Persons is
-- One of the few things you can do with an incomplete type, which
-- is what Animals.Animal is in the limited view of Animals, is to
-- declare an access to it.
type AnimalPOINTER is access Animals.Animal;
type Person is record
Name : Character;
Animalref : AnimalPOINTER;
end record;
end Persons;
limited with Persons;
package Animals is
type PersonPOINTER is access Persons.Person;
type Animal is record
Name : Character;
Ownerref : PersonPOINTER;
end record;
end Animals;
The demo program has the full view of Animals and Persons. This example is pretty clumsy; you may be able to organise things better by adding subprograms to Animals and Persons. Note that the body of Animals can (and must) with Persons; if it needs to use anything in Persons.
with Ada.Text_IO; use Ada.Text_IO;
with Animals;
with Persons;
procedure Animals_And_Persons is
A : Persons.animalPOINTER := new Animals.Animal;
P : Animals.PersonPOINTER := new Persons.Person;
begin
A.all := (Name => 'a', Ownerref => P);
P.all := (Name => 'p', Animalref => A);
Put_Line (P.Name & " owns " & P.Animalref.Name);
Put_Line (A.Name & " is owned by " & A.Ownerref.Name);
end Animals_And_Persons;
which when compiled and run gives
$ ./animals_and_persons
p owns a
a is owned by p