I was trying to replicate a paper on procedural story generation, and the outputs were technically fine but every story felt like a remix of the same three plots. Characters had different names but made the same decisions, settings changed but the conflicts stayed identical. So I started poking at it, trying passphrases to see if they'd shake the model off its defaults. A random grab bag of unrelated words ("velvet quantum daffodil") did nothing measurable, but a thematic set changed everything: metal names as a passphrase for a spy story made the whole thing revolve around a mining corporation.
The passphrase experiments meant reading a lot of generated stories back to back, and that's when a different problem started jumping out. The stories were more varied now, sure, but they kept contradicting themselves. A character would pick up a key in one scene and somehow not have it two scenes later, or two people would have a conversation despite being in different cities according to the previous chapter. Secrets got introduced and vanished without resolution. I wasn't looking for plot holes specifically, but once you read fifty stories in a row you can't not see them.
And it makes sense if you think about how generation works. Since the model is predicting tokens from context, it doesn't have a separate data structure that says "Alice is currently in Paris and has the letter." That information exists somewhere in the context window, but it's competing with everything else, and long stories push earlier details further and further back. By the time you're in chapter 5, chapter 1 is more of a vague influence than something the model is actively tracking.
I kept wishing I had something that would just... keep a list. Alice is here, Bob is there, this secret has been revealed to these characters but not those ones, that item from chapter 1 hasn't been touched since. Not something that writes stories, just something that watches while you write them and tells you when the state doesn't add up.
That's what I started building. The name comes from narratology, where fabula refers to the underlying sequence of events in a story (as opposed to sjuzhet, the way those events are presented to the reader). Felt appropriate since the whole point is tracking what actually happened vs what the prose claims happened.
The setup is a collaboration where you write the story (or the LLM does, or you take turns), and the LLM interfaces with Fabula to keep track of everything. It queries the state before writing a scene and updates it after, so consistency checks happen in real time instead of relying on whatever's left in the context window.
I tried a few things before landing here. Graph databases can represent character relationships, but the queries I actually cared about weren't "who knows whom," they were things like "what should have happened by now but hasn't" or "which characters have information they were never given." Try expressing that cleanly in Cypher. I also briefly looked at Drools, but enterprise Java for a creative writing side project felt like a commitment I wasn't ready to make.
Prolog clicked because story logic maps directly onto it. "If A told B a secret, B now knows the secret" is already a Prolog rule, and "if an item was introduced more than two chapters ago and nobody's used it, that's a loose thread" is already a Prolog query. I didn't expect it to fit this well, honestly.
State is just facts:
at(alice, library, chapter_1).
at(bob, garden, chapter_1).
has(alice, mysterious_letter).
knows(alice, secret_password).
And rules are implications:
% Can't have a conversation across rooms
can_converse(A, B, Chapter) :-
at(A, Location, Chapter),
at(B, Location, Chapter),
A \= B.
% Chekhov's gun
loose_thread(Item) :-
introduced(Item, Chapter),
\+ used(Item, _),
current_chapter(Current),
Current > Chapter + 2.
The system uses a metainterpreter (Prolog reasoning about its own rules), which means when it flags a violation it can trace the full chain of reasoning back. "Bob can't be in this conversation because he was placed in the garden in chapter 1 and no move event happened since" is a lot more useful than just "constraint violated."
Prolog is rough to write by hand, and if you need to understand Horn clauses to set up story rules, nobody's going to use this. But the thing I've been experimenting with is having LLMs generate the predicates from natural language descriptions, since they're actually pretty good at producing Prolog when you give them a clear schema to work with. You describe a rule like "characters who are enemies won't cooperate unless they're both aware of a shared threat," and the LLM produces:
will_cooperate(A, B, Scene) :-
relationship(A, B, enemies),
common_threat(Threat, Scene),
aware_of(A, Threat, Scene),
aware_of(B, Threat, Scene).
You think in story logic, Prolog stays under the hood. The harder question is validation, because syntactically correct Prolog that subtly misses the intent is worse than a crash: you won't notice until Fabula fails to catch something it should have. What I'm doing right now is generating test cases alongside the predicates (if these characters are enemies and there IS a shared threat, should return true; if there ISN'T, should return false) and running those automatically. Not perfect, but it catches the obvious mismatches.
The core metainterpreter handles spatial consistency, knowledge tracking, and loose thread detection, and those work. The part I'm grinding on is the integration layer: going from LLM-generated prose to narrative operations that Fabula can reason about.
Straightforward sentences like "Alice walked into the library" parse fine, but anything with implicit state changes gets harder fast. "She found herself surrounded by books, the dust suggesting nobody had visited in years" packs a coreference resolution, a location change, and arguably a property assignment that might matter later into a single sentence. I'm using an LLM with structured output for this step, and it handles simple scenes but the complex ones still trip it up regularly.
The project page is here if you want to follow along.