Character state

Memory, character facts, relationship, and runtime context for believable characters.

Character quality depends on state. LoreOS separates the pieces of state that make a character feel consistent, then compiles them into a runtime view when a session runs.

Main state surfaces

Persona and voice

The character’s public personality, speech style, and baseline behavior.

Character direction

The character’s long-running direction: what they want, what kind of life they are moving through, and what should make future behavior feel coherent.

Character facts and memory

Character facts are the stronger layer: stable facts about the character and world. Memory is the larger event history: conversations, moments, preferences, and repeated patterns that may or may not become stable character facts.

Relationship state

LoreOS tracks each end-user separately. The same character can have different relationship state across different sessions or external users.

Developer-facing relationship state is exposed as public-safe cards, not raw internal ledgers. For a session, start with:

GET /v1/sessions/{session_id}/relationship-snapshot

That snapshot includes qualitative relationship state, important memories, rituals, open threads, supporting cast, recent life-feed cards, identity-continuity anchors, and Telegram binding status. If your app needs a narrower surface, use the dedicated endpoints:

GET /v1/sessions/{session_id}/memories
POST /v1/sessions/{session_id}/memories
PATCH /v1/sessions/{session_id}/memories/{memory_id}
DELETE /v1/sessions/{session_id}/memories/{memory_id}
GET /v1/sessions/{session_id}/open-loops
GET /v1/sessions/{session_id}/life-feed
GET /v1/sessions/{session_id}/identity-continuity
GET /v1/sessions/{session_id}/reachout-reasons
GET /v1/sessions/{session_id}/proactive-preferences
PATCH /v1/sessions/{session_id}/proactive-preferences
POST /v1/sessions/{session_id}/telegram/bind-token
POST /v1/sessions/{session_id}/telegram/managed-child-bot
GET /v1/sessions/{session_id}/telegram/binding
DELETE /v1/sessions/{session_id}/telegram/binding

If your product needs numeric relationship state, use the explicit score endpoint:

GET /v1/me
PATCH /v1/apps/{app_id}
{ "expose_relational_numerics": true }
GET /v1/sessions/{session_id}/relationship-scores

For dating-style characters, the score keys include affection_toward_user, romantic_interest, perceived_user_affection, trust_level, comfort_level, engagement_level, boundary_level, and reciprocity_uncertainty. Values are final 0..1 state values. Each score includes updated_at and version when a relational state exists. The same opt-in meter also appears on GET /v1/sessions/{session_id}/state as relational_meter, but the dedicated relationship-scores endpoint is the clearest API to use when you are building product logic around these scores.

Dashboard export (all users / one user across characters)

The session endpoint above is one session. For a dashboard or CRM-style job, use the two export endpoints — same opt-in (PATCH /v1/apps/{app_id} {"expose_relational_numerics": true}), one final meter per session, no raw text:

GET /v1/characters/{slug}/relationship-scores?limit=50 # all users for one character
GET /v1/external-users/{external_user_ref}/relationship-scores?character={slug}&limit=50 # one user across characters

Both return relationship_score_exports[], where each row is the latest final relational state for a session and carries:

FieldMeaning
session_idthe session the meter belongs to
charactercharacter slug
external_user_refyour id for the end-user
updated_atwhen the state last changed (ISO 8601)
versionchange marker — equals the updated_at timestamp, so use it as an incremental-sync cursor (a row changed iff its version advanced), not as an integer sequence
relationship_scores[]the 0..1 dimension values (keys as on the session endpoint)

Each export is paginated with next_cursor / has_more. The character endpoint is for character-level segments or leaderboards; the external-user endpoint is for one user’s standing across characters. Neither returns raw conversation text or model traces; drill into a specific session with GET /v1/sessions/{session_id}/events?since=0 when you need the redacted event log.

score_contract explains the scale and movement semantics. Scores are not monotonic: they can move down as the relationship changes and may decay. They are directionally comparable within the same app/schema, but they are not a universal cross-app leaderboard score.

After opt-in, post-turn score commits emit relationship_score.changed on the same session event log used by polling, SSE, and webhook push. The payload contains final 0..1 values only. It does not expose raw extractor deltas, patch history, signal events, critic reasoning, or prompt text.

To inject product-owned relationship context without mutating canon/persona, use:

PATCH /v1/sessions/{session_id}/relationship-context
1{
2 "relationship_label": "founding_friend",
3 "relationship_context_key": "tier:founding_friend",
4 "write_mode": "replace_key",
5 "speakable_relationship_facts": ["The user is a founding friend."],
6 "private_relationship_context": ["Treat this as warm trust, not exclusivity."],
7 "forbidden_to_reveal": ["Do not say they are the only special user."],
8 "permanent": true
9}

This writes a session context event. Use relationship_context_key with write_mode="replace_key" to replace one active app-owned context row. Use permanent=true only for context that should stay active until deleted.

GET /v1/sessions/{session_id}/relationship-context
DELETE /v1/sessions/{session_id}/relationship-context/{context_event_id}

Relationship-context writeback does not overwrite character canon, persona, or learned relational scores. It is planning-guarded, so supplied relationship facts should not be contradicted by generic first-meeting or “too early to say” assumptions. The guard does not authorize romance, permanence, exclusivity, or claims beyond the supplied facts.

Use this, not memories, for a product-owned tier. A memory is a lived event the character learned — it can be cited and embellished back to the user. A product tier (“founding friend”, “inner-circle”, a paid plan) is a label you own that should shape the character’s stance without becoming a spoken discovery. So write tiers as relationship-context (the speakable + private + forbidden lanes), not as POST /v1/sessions/{session_id}/memories. Put the stance you want voiced in speakable_relationship_facts, the stance that should only steer behavior in private_relationship_context, and hard “never say” lines in forbidden_to_reveal.

TTL vs permanent. A context row expires after about 10 minutes by default (ttl_seconds, max 86400). Set permanent: true to keep it active until you DELETE it. Use permanent for durable tiers (the “founding friend” label should not silently lapse mid-conversation), and the default TTL for transient context (today’s mood, the scene the user is currently in).

write_mode (avoid duplicate tiers). To update a tier without stacking duplicate active rows, send a stable relationship_context_key (e.g. "tier:founding_friend") with write_mode: "replace_key" — LoreOS soft-deletes the active rows that share that key before storing the new one (replace_key requires the key). Omit write_mode (it defaults to append) when you intend to add context alongside what is already active.

Safety and session lifecycle

Use lifecycle controls when your product needs pause, block, report, or deletion flows:

PATCH /v1/sessions/{session_id}/lifecycle
POST /v1/sessions/{session_id}/block
POST /v1/sessions/{session_id}/report
DELETE /v1/sessions/{session_id}/memories/{memory_id}
POST /v1/external-users/{external_user_ref}/delete

PATCH /lifecycle accepts action: "pause" | "resume". Paused sessions reject new messages with 409 session_paused and are skipped by active-session runtime/proactive work. Pause does not delete memory, mutate canon, or erase usage records.

POST /block exits only this user-character relationship. It emits session.exited, sets lifecycle_state=exited, and cannot be resumed. It is not an app-wide user block and it does not delete memory.

POST /report emits a safety.reported event for your support/moderation workflow. It does not automatically pause, block, forward, or delete anything; pair it with lifecycle, block, memory deletion, or external-user deletion according to your own product policy.

These endpoints are app-scoped and projection-only. They do not expose raw prompts, critic traces, private Story Room plans, raw world-model rows, raw offscreen transcripts, or provider payloads. The numeric score endpoint also does not expose raw extractor deltas, patch history, signal events, or critic reasoning.

World context

World context describes recurring places, people, routines, obligations, and event sources that make a character’s life feel grounded.

Safe runtime context

Not every internal fact should be visible to the chat actor. LoreOS builds a safe runtime context that excludes private plans, future forecasts, and unapproved candidates.

Why this matters

A simple prompt tends to flatten everything into one context window. LoreOS keeps state as typed ledgers and compiled projections, so a character can:

  • remember different users separately;
  • avoid leaking private future plans;
  • evolve slowly instead of changing personality every turn;
  • produce replies and images that match the current world state;
  • expose inspectable state through API and observability surfaces.

Use GET /v1/characters/{slug}/runtime-preview to inspect what the runtime will use for a character, GET /v1/characters/{slug}/readiness before launch, and GET /v1/sessions/{session_id}/relationship-snapshot when you need relationship evidence for one end-user session.