Inspiration
We have spent the last two years running harpy.chat, an AI character chat platform. Operate one of these long enough and you stop thinking in categories and start thinking in people: the reader who opens the same character every night for a week and then quietly never comes back; the creator who posts fourteen characters, watches one take off, and cannot tell you why that one.
The industry numbers are loud — readers spend 40 hours a month in character chat, more per person than YouTube or Netflix, and in Korea it is the most-used AI category by hours, ahead of ChatGPT by more than 2×. But operating teaches you what the numbers don't: engagement here works like Shorts. People want to be inside a story within seconds, and then they want endless variants of it. A character card that is a name, three adjectives and a greeting delivers the first and starves the second. It plays well for ten minutes, because there is no world behind it to vary.
And we kept meeting the people who have that world. One of us leads a community of fan-fiction writers in Korea who want to cross into character chat. They have casts, histories, consequences earned across a hundred chapters — everything the format is starving for. And that work gets read once, in an afternoon, and is done. We spent real effort explaining what works: how to open, where to stop, what makes a scene playable. The explanations rarely survived contact with a blank page. Somewhere in those conversations the idea flipped: stop explaining the format. Build the format into a tool, and let their finished work carry it.
The last piece was structural. Read the best character chats next to serialised episodes and they are the same material. An episode delivers setup, development, the turn, resolution — in order, and it ends. A great first message is that arc with the ending torn off: everything up to the turn in one message, the resolution handed to the reader as choices. Not a shorter story. A story cut in a different place.
If a chat scene is a story cut differently, every finished book already contains hundreds of them. Nobody had built the thing that finds the cut.
What it does
OpenEnds compiles a finished book into playable character scenes.
The compiler reads the work once and produces a timeline of beats with who was present at each, a world model of the cast and the rules they live under, and a causal graph: which beat caused which later beat, through what mechanism, under what trigger condition.
From that it does two things a tool without compiled state cannot.
It finds where the story can branch. For a beat $b$, walk the causal graph's downstream closure $D(b)$ (depth-capped) and score
$$L(b) = \sum_{b' \in D(b)} \frac{\pi(b \to b')}{d(b, b')}, \qquad \alpha(b) = \big|{(c_i, c_j) \text{ present} : \exists f,\ f \in K_{c_i} \wedge f \notin K_{c_j}}\big|$$
— leverage (how much of the book depends on this beat, confidence-weighted and distance-discounted) and knowledge asymmetry (how many pairs in the room differ in what they know). A branch point needs both: leverage says a choice would matter, asymmetry says someone in the room can act on what another does not know. Most moments have neither. In one Sherlock volume there are 3,631 causal edges; walking them shows that beat 99 — Wilson describing a newspaper advertisement — has 630+ downstream beats, while the dramatic-sounding "Watson accepts the night mission" has zero.
It writes from one character's point of view. A character $c$ receives the projection $\sigma\big(\pi_c(s_k),\, T_c(k)\big)$: the omniscient state filtered to what $c$ perceives, a timeline restricted to beats where $c$ was present, and a substitution $\sigma$ that renames concealed identities — grammatical gender included — to what $c$ was told. A character is a filtered view of the compiled work. Play Telemachus in Book I of the Odyssey and you meet a weathered sea-captain named Mentes. You do not learn that this is Athena, because Telemachus was not present when she flew down from Olympus — and the compiled context he receives has that beat removed, her name replaced with the one he was given, and even the pronouns rewritten to match the disguise.
The illustrations come from the same compiled state, so the pictures keep the secret too.
How we built it
Under the hood, generation is two steps, because the two jobs fight each other when combined: a schema-forced call decides what happens and updates the world state, then a stylist writes the prose from those beats and is forbidden from inventing events. Formally the story is an MDP $(S, A, P, R)$ — the three choices a reader picks from are simply $A$ made visible at the turn.
Two early decisions did most of the work. Memory is state, not context: every generation sees recent prose, recent beats, and the world model, never the whole book, so chapter forty costs what chapter one does. The library is retrieved, not loaded: we measured a whole book in context at 41,000 tokens and 43–69 seconds a call even with a 98% cache hit — and a 700,000-token novel never fits at all. Retrieving the relevant ~2% answers in seconds.
The part we ended up caring about most is the point-of-view boundary — four layers, each added only after we caught a leak: filter the state, drop unwitnessed beats, alias concealed identities, rewrite their pronouns. Because the moment a reader catches a character knowing something they could not know, the spell breaks — and readers do not file a bug, they just leave.
Challenges we ran into
The honest answer is that the compiler was never the plan. It is what survived.
We tried the obvious thing first — the whole book in the context window — and watched the clock ruin it. We tried a verify-everything annotation pipeline; it finished 3 books out of 159. We built a generation harness that genuinely rescued a small local model, then watched the same harness drag a frontier model from 91 down to 79 on a blind score, because guardrails that stop a weak model looping are a whip on a capable one. Each failure took an option off the table, until what remained was the compiler: read the book once, keep the state, retrieve the rest.
The other constraint came straight from the platform: people do not play what they do not understand. A reader gives a new scene a few seconds — the card image, the character line, the opening — and if they cannot tell who they are, who is in front of them, and what is at stake, they are gone. So every surface the pipeline produces has to be concise, direct, and still complete: the whole of setup, development and the turn in 200–450 words; a cast line that says who you play and who you face; images generated from the same compiled state so they explain the scene instead of decorating it. Getting one pipeline to hold all three at once was harder than any single model call, and most of our schema checks exist to enforce exactly that balance.
Even knowing whether something worked was a fight. Identical code with a different seed moved a blind score by ten points, so half of what we celebrated as improvements turned out to be noise until we rebuilt evaluation around shuffled multi-pass judging and metrics that need no judge at all.
And then the compiler had to earn its central promise — only what they saw — one leak at a time. The same disguise bug had to be fixed in three places: image prompts, text prompt, and finally the compiled context itself, because the evidence said "receives her spear" and you cannot instruct a model out of its evidence. A model fabricated a citation for a fact our filter had correctly removed. And twice our own rails stopped us, correctly — once refusing a faster model that would have made the books incomparable, once killing a run rather than build world-state from a translator's preface our fetcher had let through. We were grateful for both, eventually.
Accomplishments that we're proud of
The one we tell people about first: we let the data overrule us. We measured 10,320 public character cards against aggregate engagement — no users, no conversations, just which published cards hold readers — and the analysis contradicted our own design. Second-person address turned out to be the strongest predictor of a reader staying (δ ≈ 0.21, in both content strata, after controlling for length), and the best openings run 200–450 words; our three-paragraph cap had real output stuck at the bottom of that band. We changed the schema the same day, and said so where we were wrong.
The comparison page is built so it cannot flatter us: three scenes, each opened twice by the same model on the same budget, presented blind as A and B — you read first, then reveal. On the Odyssey, the raw-pages version copies 31.53% of its eight-word sequences straight from Butler,
$$V(t) = \frac{|G_8(t) \cap G_8(B)|}{|G_8(t)|},$$
and the compiled version copies 0.00%.
The point-of-view boundary held up when we attacked it: across all 32 compiled books and every character, the alias layer fires on exactly four books — every one a genuine disguise — and leaves everything else byte-identical. And behind all of it, 834 tests, one pinning every leak we ever found.
What we learned
Every lesson pointed the same direction: do not trust generation — validate it.
Models absorb whatever you show them. An example rule we wrote into a prompt — "an auditor may not audit kin" — came back as a discovered rule of the book. It read perfectly — it was invented to. The plausible fabrication is the dangerous one: it survives review and steers everything downstream.
So compilation became hypothesis-and-validation. Every beat is a small scientific claim: a generator proposes the state, a separate verifier tries to reject it, and a claim that cannot survive validation stops the run rather than being kept. In our testing, Gemini 3.7 Flash proposes strong state hypotheses and validates them reliably; at $0.75 per million input tokens, the once-per-book compile is practical. Compile once, and every scene, branch, and illustration is served from validated state — cheap, fast, and checkable.
And the same lesson turned out to apply to people. We could not lecture fan-fiction writers into a format, however carefully we explained it — but the rules that matter — always ship a first message, address the reader, stop on the turn — now live in the schema and its checks, where nobody has to remember them. Knowledge that is not enforced is lost. The tool is the lecture, made executable.
What's next for OpenEnds
Prove the sentences come from the book. Zero verbatim overlap follows from architecture, not evidence — it does not show the prose is derived from the compiled book rather than the model's memory of it. We caught exactly that leaking in: a detail about Penelope's shroud that appears nowhere in the context and 255,000 characters later in the text. The test is counterfactual — compile a variant with inverted facts and see which one the output tracks. If the book wins, the compiler is real.
Put it in front of the writers we already know. It works on public-domain books because that is what we could test on. The first real users are the fan-fiction community we lead in Korea — the same writers who have been asking us what works in character chat. Now the answer is a tool instead of a lecture, and their work will break assumptions ours did not.
Close the loop. Today we compile a finished work into scenes. The version we think about most runs backwards: which turn readers keep playing is a signal about where a story carries tension — and that signal belongs back with the person writing the next chapter.
One belief runs under all of this. The stories that hold people will not come from training a larger model, because a model has not lived anything — it can interpolate a childhood, but it has never had one. The consequences that make a scene cost something are earned by a person who has been somewhere and lost something, and that is exactly the part our system refuses to generate and compiles instead. We think the creator is not a casualty of this technology but its most durable job: the human who lives a story so that thousands of others can step inside it. OpenEnds is built on that bet. The model does the assembling. A person does the living.
Log in or sign up for Devpost to join the conversation.