Splitting a Companion in Two: the ANP-neo / Compass-neo Architecture

A conversation core with no memory of its own, and a memory service that never calls an LLM. The shape of my AI companion, in one diagram and three rules.

  • architecture
  • llm
  • memory

My AI companion used to be one program that did everything. It is now two services and a side job.

                     ┌────────────────┐
   me ◀───────────▶│    ANP-neo     │  conversation core (Go)
                     │   no memory    │  owns dialogue + streaming
                     └───────┬────────┘
                             │  POST /embed   POST /search
                     ┌────────────────┐
                     │  Compass-neo   │  memory service (Python)
                     │  NEVER an LLM  │  store · embed · rank
                     └──┬──────────┬──┘
             verbatim   │          │   crystals
             (raw log)  │          │   (distilled facts)
                        ▼          ▲
                     ┌────────────────┐
                     │  Crystallizer  │  micro-service; the LLM lives here
                     │  poke ▸ distil │  trigger ≠ execute
                     └────────────────┘

Three rules hold it together.

ANP-neo keeps no memory. When it wants context, it asks over HTTP.

Compass-neo never calls an LLM. Store, embed, rank — that is the whole contract. No prompt, no model choice, no temperature. So when it misbehaves, the cause is storage, embedding, or ranking. It is never "the model had an off day."

A crystal is a unit of meaning, not a unit of text. Not every 500 characters — one distilled fact, written to stand alone, so a search hit is a complete statement rather than half an idea.

Distillation is triggered by a fire-and-forget poke when a reply lands, and never blocks the conversation. If the Crystallizer is down, the messages stay marked unprocessed and the next poke collects them.


I hope this is useful to anyone who likes talking with an LLM — not only using one as a tool.