Chapter Four
PrOOPt States — Getting Started
In which objects acquire inner lives, and the inner lives acquire a database schema.
What Is PrOOPt States?
PrOOPt core governs what an LLM may do. PrOOPt States governs what an LLM may remember and become.
Here is the idea in one breath. You write a plain Java object. You
describe what it is in a sentence on a @Persona
annotation. You describe each of its fields in a sentence on a
@Trait annotation. From then on, an LLM manages how those
fields change — in response to events you send, on schedules you set,
and even during quiet hours when the entity sits alone and reflects.
Every change is persisted to ordinary database tables, every change
has a recorded source, and the whole population is queryable with
SQL-style predicates.
The developer describes what the entity is. The LLM decides what it becomes. The database remembers everything either of them did.
The Dependency
<dependency>
<groupId>io.github.argonizer</groupId>
<artifactId>prooopt-states</artifactId>
<version>${prooopt.version}</version>
</dependency>
<!-- Or, for Spring Boot users who enjoy nice things: -->
<dependency>
<groupId>io.github.argonizer</groupId>
<artifactId>prooopt-states-spring-boot-starter</artifactId>
<version>${prooopt.version}</version>
</dependency>
The starter wires the persona engine, the schedulers for evolution
and internal loops, and the event pipeline into your Spring context.
Personas persist via JPA by default
(@Persona(store = Store.JPA)), and all personas share the
same five physical tables — including
prooopt_persona_history, which will quietly become your
favourite table.
Your First Persona
Let us hire a castle guard. He is not a chatbot. He is state.
@Persona(
value = "A city guard in a medieval fortress town. Dutiful but "
+ "underpaid, loyal to his captain, suspicious of strangers, "
+ "and fond of gossip on the night shift.",
evolutionSchedule = "@daily",
evolutionDescription = "Fatigue accumulates on long shifts and "
+ "recovers on rest days. Old grievances fade slowly unless "
+ "refreshed. Morale drifts toward the company's mood."
)
public class GuardNpc {
@PersonaId
private String guardId;
@Trait("Morale from 0 (deserting at dawn) to 100 (would die "
+ "for the captain). Rises with pay and victories; falls "
+ "with hunger, fear, and unpaid wages.")
private int morale;
@Trait("Suspicion toward the player, 0–100. Rises sharply when "
+ "crimes are witnessed; rises slowly on rumours; decays "
+ "only with time and good deeds.")
private int suspicion;
@Trait(value = "Private running memory of recent encounters, "
+ "in the guard's own words.", index = false)
private String memory;
}
Read those @Trait descriptions again. Each one is a
miniature law of physics for that field. “Rises sharply when crimes
are witnessed; decays only with time” is not a comment — it is the
contract the LLM is held to every time it updates the field. Rich,
specific descriptions produce better state transitions. Vague ones
produce a guard with the emotional consistency of a weather vane.
Bring Him to Life
The PersonaStateEngine has a small, honest lifecycle:
initialise, update, evolve, run a loop, retire, restore, load.
GuardNpc guard = new GuardNpc();
engine.initialise(guard, meta, "guard-042",
"A veteran of the northern gate, ten years of service");
// Something happens in the world:
PersonaStateDiff diff = engine.update(guard, meta,
"The player was seen picking the lock on the granary door "
+ "at midnight.");
System.out.println(guard.getSuspicion()); // noticeably higher
System.out.println(diff); // exactly what changed, and why
The engine builds a prompt from the persona description, the trait
contracts and the event; the LLM proposes new values; the engine
parses, validates, writes the fields, and records the change in
prooopt_persona_history with its source. You get back a
PersonaStateDiff — the before, the after, and the
reasoning. No mystery, no vibes. Rows.
What “Persisted and Queryable” Buys You
Because every trait is indexed by default, a population of personas can be queried like inventory:
List<GuardNpc> disgruntled =
guards.loadWhere("morale < 20 AND suspicion > 70");
That single line is the difference between PrOOPt States and a
pile of chat transcripts. State lives in tables. History has source
attribution — was this change caused by an external event, social
contagion, scheduled evolution, or the entity's own internal
reflection? (UpdateSource knows.) And large text blobs
like narrative memory can opt out of indexing with
index = false, because nobody needs a B-tree
over a guard's diary.
Where to Next?
You have one guard with feelings and a history table. The States Tutorials scale this up: generating whole populations with census-shaped distributions, broadcasting events with blast radii and rumour distortion, scheduled evolution, internal thought loops, deterministic veto power, and the longitudinal reports that make it all legible.
