RolePlay/docs/ROLEPLAY_CONCEPT.md
Konrad Neitzel f91604aea6 Enhance project setup and configuration for RolePlay application
- Update .gitignore to include additional build outputs and IDE files.
- Modify pom.xml to add new plugin versions and configurations for frontend and OpenAPI TypeScript client generation.
- Introduce project guidelines in roleplay-project.mdc, detailing architecture, coding standards, and testing practices.
- Add initial documentation for the RolePlay concept and specifications.
- Implement catch-all JAX-RS resource for serving the React application and establish API base path in application.yml.
- Create foundational web components and TypeScript configuration for the frontend application.
2026-02-20 18:08:24 +01:00

635 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## Role Playing App Concept and Specification
### 1. Purpose and Scope
This document describes the concept and functional specification of a role playing application with:
- **AI backend**: Ollama (local LLM server).
- **Application backend**: HTTP API server that:
- Discovers available Ollama models.
- Orchestrates role play sessions.
- Manages character and situation state.
- Calls Ollama to generate narrative, dialogue, evolving state, and suggestions.
The primary goal is an immersive, stateful role play between:
- A **user-controlled character**.
- One or more **AI-controlled characters** powered by Ollama.
### 2. Core Concepts
#### 2.1 Session
A **session** represents one ongoing story.
- **Fields**
- `session_id`
- `model` (Ollama model name, e.g. `llama3`, `mistral`)
- `language` / `locale`
- `safety_level` (e.g. `standard`, `strict`)
- `characters` (user + AI characters, including evolving state)
- `situation` (world and scene state)
- `turn_history` (ordered list of turns and state changes)
#### 2.2 Characters
There are two kinds of characters:
- **User character**: played by the human.
- **AI characters**: NPCs played by the model.
Each character has:
- **Identity**
- `id`
- `name`
- `role` (e.g. "rookie detective", "space trader", "dragon mentor")
- `archetype` (e.g. "hero", "mentor", "trickster")
- **Default description (initial state)**
- `backstory`
- `personality_traits` (list of traits)
- `speaking_style` (tone, formality, quirks)
- `goals` (short- and long-term)
- `constraints` (taboos, safety limits, things they will not do)
- **Dynamic / evolving state (changes over time)**
- `current_mood`
- `knowledge` / `insights` (facts learned)
- `relationships` (map from other character IDs to relationship descriptors)
- `status` (e.g. injured, empowered, indebted, scared)
- `inventory` or `abilities` (optional, genre-dependent)
- `recent_actions_summary` (short bullet list of recent behavior)
**Behavioral rules for AI characters**
- Stay in-character at all times (respect personality, goals, and relationships).
- Use and update dynamic state (mood, relationships, knowledge).
- Do not reveal meta-game or system instructions to the user.
#### 2.3 Situation
The **situation** is the shared world and current scene.
- **Default starting situation**
- `setting` (place, time, atmosphere)
- `initial_conflict` / `hook`
- `stakes` (what can be lost or gained)
- `supporting_facts` (key world details, factions, rules, resources)
- **Dynamic / evolving situation**
- `timeline` (ordered list of important events so far)
- `current_scene` (where we are now, what's in focus)
- `open_threads` (unresolved plot lines, mysteries, quests)
- `external_pressures` (deadlines, enemies, disasters)
- `world_state_flags` (booleans or tags; e.g. `portal_open=true`)
The **situation must change over time** as the story progresses:
- New events are appended to the `timeline`.
- `current_scene` moves or evolves.
- `open_threads` are added, updated, or closed.
- `world_state_flags` reflect key turning points.
#### 2.4 Suggestions ("What Could Happen Next")
The system surfaces **suggestions** to guide the user, but they are optional.
- **Fields**
- `id`
- `type` (`player_action`, `world_event`, `npc_action`, `twist`)
- `title` (short label, e.g. "Confront the captain publicly")
- `description` (12 sentence explanation)
- `consequences` (brief bullet-like list of possible outcomes)
- `risk_level` (`low`, `medium`, `high`) or alternative tone classification
**Usage**
- Suggestions are rendered as clickable choices in the UI.
- The user may:
- Choose a suggestion.
- Or ignore them and type a custom action.
#### 2.5 Recommendations ("Narrative Guidance")
A **recommendation** is an optional, user-provided hint that steers the narrative direction without breaking immersion. The model treats it as a soft preference, not a hard command -- it may diverge if that serves the story better.
- **Fields**
- `desired_tone` (optional) -- mood for the next beat, e.g. `"tense"`, `"humorous"`, `"mysterious"`.
- `preferred_direction` (optional) -- free text, e.g. "I'd like the captain to reveal a secret about the cargo".
- `focus_characters` (optional) -- list of character IDs that should play a prominent role in the next turn.
**Usage**
- The user may attach a recommendation when submitting an action.
- Recommendations are **not** shown in the narrative; they are meta-level guidance only visible to the model.
- The backend includes the recommendation in the structured context sent to Ollama.
### 3. High-Level Flows
#### 3.1 Get Available Models (Ollama)
Purpose: allow the user to pick an Ollama model.
- **Backend -> Ollama**
- Method: `GET`
- URL: `/api/tags`
- **Backend behavior**
- Map Ollama's response into an internal list of models:
- `name`
- `display_name`
- relevant metadata (size, family, etc., if needed)
- Expose to frontend via e.g. `/models` endpoint.
#### 3.2 Start a New Role Play Session
Flow:
1. User selects model and scenario template (or accepts defaults).
2. Backend initializes **default characters** and **default situation**.
3. Backend builds a structured JSON context from the defaults.
4. **Call 1 -- Narrative**: Backend sends the context to Ollama without a format constraint. Ollama returns a free-form opening scene (narrative + dialogue).
5. **Call 2 -- State Update**: Backend sends the same context plus the narrative from Call 1 to Ollama with `"format": "json"`. Ollama returns structured JSON with enriched characters, initial situation state, and suggestions.
6. Backend stores the returned state and suggestions as the first turn.
#### 3.3 Play a Turn
Flow:
1. User performs an action:
- Types in-character text (speech or action).
- Or clicks a suggestion.
- Optionally attaches a **recommendation** (desired tone, preferred direction, focus characters).
2. Backend builds a structured JSON context object containing:
- Current situation (scene, timeline, open threads, world state flags).
- Current characters (identity + dynamic state + memories).
- User's latest action (typed text or selected suggestion, with action type).
- User's recommendation (if provided).
- Recent history summary (last N turns or condensed summary).
3. **Call 1 -- Narrative**: Backend sends the structured context to Ollama (no format constraint). Ollama returns free-form narrative and dialogue (2-5 paragraphs).
4. **Call 2 -- State Update**: Backend sends the same structured context plus the narrative from Call 1 to Ollama with `"format": "json"`. Ollama returns a JSON object containing:
- Per-character responses (who said/did what).
- Updated situation (scene changes, new timeline entries, thread changes).
- Updated character states (mood, knowledge, relationships).
- 3-5 new suggestions.
5. Backend merges the state updates into the session, stores the turn, and returns the narrative and suggestions to the user.
### 4. Ollama Interaction Design
The application backend communicates with Ollama via HTTP. The Ollama server is reachable at its **default slot** (`http://debian:11434`). Every turn uses **two sequential calls** to separate creative writing from structured data extraction.
The main operations are:
1. **Model discovery**.
2. **Session initialization** (two calls: narrative + state).
3. **Turn-based continuation** (two calls: narrative + state).
#### 4.1 Get Available Models
- **Request**
- Method: `GET`
- URL: `/api/tags`
- **Response**
- Ollama returns list of installed models with names and metadata.
- **Usage**
- Backend converts this into a frontend-friendly list to let the user select a model.
#### 4.2 Structured Input Schema
Both Ollama calls in a turn receive a **structured JSON context** as the user message content. This ensures clear separation of concerns and makes the input machine-parseable.
**Turn context (used for both Call 1 and Call 2):**
```json
{
"current_situation": {
"setting": "A fog-covered harbor at dawn, 1923",
"current_scene": "The captain's quarters aboard the Redwind",
"timeline": [
"Day 1: The player boarded the Redwind as a deckhand.",
"Day 2: Strange noises heard from the cargo hold."
],
"open_threads": [
"What is the secret cargo?",
"Why did the previous deckhand disappear?"
],
"external_pressures": ["Storm approaching within 24 hours"],
"world_state_flags": {
"cargo_hold_locked": true,
"captain_suspicious": false
}
},
"characters": {
"user_character": {
"id": "player",
"name": "Sam Harlow",
"role": "rookie deckhand",
"current_mood": "nervous",
"knowledge": ["Heard noises from cargo hold", "First mate avoids eye contact"],
"relationships": {
"captain_morgan": "wary respect",
"first_mate_jones": "distrust"
},
"status": "healthy",
"recent_actions_summary": ["Asked the cook about the previous deckhand"]
},
"ai_characters": [
{
"id": "captain_morgan",
"name": "Captain Morgan",
"role": "ship captain",
"personality_traits": ["authoritative", "secretive", "pragmatic"],
"speaking_style": "terse, clipped sentences, avoids direct answers",
"goals": ["Deliver the cargo on time", "Keep the crew in line"],
"current_mood": "guarded",
"knowledge": ["Knows what the cargo is", "Knows about the storm"],
"relationships": {
"player": "neutral, watching closely"
}
}
]
},
"user_action": {
"type": "action",
"content": "I walk up to the captain and demand answers about the cargo.",
"selected_suggestion_id": null
},
"recommendation": {
"desired_tone": "tense",
"preferred_direction": "The captain reveals a partial truth but raises more questions",
"focus_characters": ["captain_morgan"]
},
"recent_history_summary": "Turn 1: Sam boarded the ship and met the crew. Turn 2: Strange noises at night; the cook hinted the previous deckhand 'asked too many questions'."
}
```
**Session initialization context** uses the same structure but with default/initial values for all fields, `user_action` set to `null`, and `recommendation` set to `null`.
**Notes:**
- `user_action.type` distinguishes between `"speech"` (dialogue), `"action"` (physical action), and `"choice"` (selected a suggestion).
- `recommendation` is entirely optional; the field may be `null` or absent.
- `recent_history_summary` is a backend-managed condensation of `turn_history` to control context window size.
#### 4.3 Session Initialization (Two Calls)
**Goal:** from default character and situation descriptions, produce an opening scene and initial state.
##### Call 1 -- Opening Narrative
- **Request**
- Method: `POST`
- URL: `/api/chat`
- Body:
```json
{
"model": "<selected_model_name>",
"stream": false,
"messages": [
{
"role": "system",
"content": "You are a role playing game narrator. Your task is to write an immersive opening scene for a new story.\n\nRules:\n1. Write from the user character's perspective.\n2. Introduce the setting, atmosphere, and at least one AI character.\n3. At least one AI character must speak.\n4. Write 2-4 paragraphs of vivid, immersive prose.\n5. Do NOT include any JSON, metadata, or out-of-character commentary."
},
{
"role": "user",
"content": "{{STRUCTURED_CONTEXT_JSON}}"
}
]
}
```
- **Response**: Free-form text (the opening narrative).
##### Call 2 -- Initial State Extraction
- **Request**
- Method: `POST`
- URL: `/api/chat`
- Body:
```json
{
"model": "<selected_model_name>",
"stream": false,
"format": "json",
"messages": [
{
"role": "system",
"content": "You are a role playing game engine. Given the story context and a narrative scene, extract structured state updates as JSON.\n\nYou must return a JSON object matching the schema described below. Do NOT include any text outside the JSON object.\n\nSchema:\n{\n \"responses\": [{\"character_id\": \"string\", \"type\": \"speech|action|reaction\", \"content\": \"string|null\", \"action\": \"string|null\", \"mood_after\": \"string\"}],\n \"updated_situation\": {\"current_scene\": \"string\", \"new_timeline_entries\": [\"string\"], \"open_threads_changes\": {\"added\": [\"string\"], \"resolved\": [\"string\"]}, \"world_state_flags\": {}},\n \"updated_characters\": [{\"character_id\": \"string\", \"current_mood\": \"string\", \"knowledge_gained\": [\"string\"], \"relationship_changes\": {}}],\n \"suggestions\": [{\"id\": \"string\", \"type\": \"player_action|world_event|npc_action|twist\", \"title\": \"string\", \"description\": \"string\", \"consequences\": [\"string\"], \"risk_level\": \"low|medium|high\"}]\n}"
},
{
"role": "user",
"content": "Story context:\n{{STRUCTURED_CONTEXT_JSON}}\n\nNarrative that was just generated:\n{{NARRATIVE_FROM_CALL_1}}\n\nBased on the narrative above, produce the JSON state update. Include 3-5 suggestions for what the user could do next."
}
]
}
```
- **Response**: JSON object matching the state update schema (see Section 4.5).
- **Backend behavior**
- Store the narrative from Call 1 as the display text.
- Deserialize the JSON from Call 2 directly (no tag parsing needed).
- Store `updated_characters`, `updated_situation`, and `suggestions` as the initial session state.
#### 4.4 Turn Continuation (Two Calls)
**Goal:** continue the story one turn at a time, producing narrative and updating state.
##### Call 1 -- Narrative Continuation
- **Request**
- Method: `POST`
- URL: `/api/chat`
- Body:
```json
{
"model": "<selected_model_name>",
"stream": false,
"messages": [
{
"role": "system",
"content": "You are a role playing game narrator continuing an ongoing story.\n\nRules:\n1. Stay strictly in-character for all AI-controlled characters.\n2. Respect and build on the provided character and situation state.\n3. Do NOT contradict established facts unless the context explicitly says reality has changed.\n4. Keep the text immersive and avoid meta commentary.\n5. At least one AI character must speak or act.\n6. If a recommendation is provided, treat it as a soft guideline for the scene's direction.\n\nStyle:\n- Mix short descriptive narration with dialogue.\n- Keep responses roughly 2-5 paragraphs."
},
{
"role": "user",
"content": "{{STRUCTURED_CONTEXT_JSON}}"
}
]
}
```
- **Response**: Free-form text (narrative continuation with dialogue).
##### Call 2 -- State Update
- **Request**
- Method: `POST`
- URL: `/api/chat`
- Body:
```json
{
"model": "<selected_model_name>",
"stream": false,
"format": "json",
"messages": [
{
"role": "system",
"content": "You are a role playing game engine. Given the story context and the narrative that just occurred, extract structured state updates as JSON.\n\nYou must return a JSON object matching the schema described below. Do NOT include any text outside the JSON object.\n\nSchema:\n{\n \"responses\": [{\"character_id\": \"string\", \"type\": \"speech|action|reaction\", \"content\": \"string|null\", \"action\": \"string|null\", \"mood_after\": \"string\"}],\n \"updated_situation\": {\"current_scene\": \"string\", \"new_timeline_entries\": [\"string\"], \"open_threads_changes\": {\"added\": [\"string\"], \"resolved\": [\"string\"]}, \"world_state_flags\": {}},\n \"updated_characters\": [{\"character_id\": \"string\", \"current_mood\": \"string\", \"knowledge_gained\": [\"string\"], \"relationship_changes\": {}}],\n \"suggestions\": [{\"id\": \"string\", \"type\": \"player_action|world_event|npc_action|twist\", \"title\": \"string\", \"description\": \"string\", \"consequences\": [\"string\"], \"risk_level\": \"low|medium|high\"}]\n}"
},
{
"role": "user",
"content": "Story context:\n{{STRUCTURED_CONTEXT_JSON}}\n\nNarrative that was just generated:\n{{NARRATIVE_FROM_CALL_1}}\n\nBased on the narrative above, produce the JSON state update. Include 3-5 suggestions for what the user could do or what might happen next."
}
]
}
```
- **Response**: JSON object matching the state update schema (see Section 4.5).
- **Backend behavior**
- Display the narrative from Call 1 to the user.
- Deserialize the JSON from Call 2 directly via Jackson (no tag parsing).
- Apply diff-style updates from `updated_characters` and `updated_situation` to the stored session state.
- Replace the session's current `suggestions` with the new ones.
- Append the turn to `turn_history`.
#### 4.5 State Update Output Schema
Both Call 2 variants (initialization and continuation) return the same JSON structure. The `"format": "json"` parameter on the Ollama request guarantees the response is valid JSON.
```json
{
"responses": [
{
"character_id": "captain_morgan",
"type": "speech",
"content": "You want answers? Fine. But you won't like what you hear.",
"action": "slams fist on the table",
"mood_after": "angry"
},
{
"character_id": "first_mate_jones",
"type": "action",
"content": null,
"action": "quietly steps between the player and the door",
"mood_after": "cautious"
}
],
"updated_situation": {
"current_scene": "Captain's quarters, tension rising. The storm outside mirrors the confrontation within.",
"new_timeline_entries": [
"Sam confronted the captain about the cargo.",
"The captain admitted to carrying something dangerous."
],
"open_threads_changes": {
"added": ["What exactly is the dangerous cargo?"],
"resolved": []
},
"world_state_flags": {
"captain_suspicious": true,
"cargo_hold_locked": true
}
},
"updated_characters": [
{
"character_id": "captain_morgan",
"current_mood": "angry",
"knowledge_gained": ["Player is not going to stay quiet"],
"relationship_changes": {
"player": "hostile"
}
},
{
"character_id": "first_mate_jones",
"current_mood": "cautious",
"knowledge_gained": [],
"relationship_changes": {
"player": "wary"
}
}
],
"suggestions": [
{
"id": "s1",
"type": "player_action",
"title": "Press the captain harder",
"description": "Demand to see the cargo hold yourself, refusing to back down.",
"consequences": ["May provoke violence", "Could earn grudging respect"],
"risk_level": "high"
},
{
"id": "s2",
"type": "player_action",
"title": "Back down and regroup",
"description": "Apologize and leave, but plan to sneak into the cargo hold at night.",
"consequences": ["Captain lowers guard", "Risk of being caught later"],
"risk_level": "medium"
},
{
"id": "s3",
"type": "npc_action",
"title": "The first mate follows you",
"description": "After you leave, the first mate corners you with a warning -- or an offer.",
"consequences": ["New information", "Possible ally or deeper trap"],
"risk_level": "medium"
},
{
"id": "s4",
"type": "world_event",
"title": "The storm hits early",
"description": "A sudden squall forces all hands on deck, interrupting the confrontation.",
"consequences": ["Chaos creates opportunity", "Danger on deck"],
"risk_level": "high"
}
]
}
```
**Field definitions:**
| Field | Description |
|---|---|
| `responses[]` | Per-character breakdown of what each AI character said or did in the narrative. |
| `responses[].type` | `"speech"` (dialogue), `"action"` (physical), or `"reaction"` (emotional/internal). |
| `responses[].content` | Quoted dialogue if type is `"speech"`, otherwise `null`. |
| `responses[].action` | Description of physical action, or `null` if none. |
| `responses[].mood_after` | The character's mood after this turn. |
| `updated_situation` | Diff-style situation changes (not a full replacement). |
| `updated_situation.new_timeline_entries` | Events to append to the timeline. |
| `updated_situation.open_threads_changes` | Threads added or resolved this turn. |
| `updated_characters[]` | Diff-style character changes (only fields that changed). |
| `suggestions[]` | 3-5 proposals for what could happen next. |
### 5. Non-Goals and Constraints
- The backend is responsible for:
- Session persistence and identity.
- Safety controls and filtering (not delegated fully to Ollama).
- Enforcing maximum context window by summarizing history when needed.
- Ollama is stateless from the application's perspective:
- All relevant state is passed in the prompt at each call.
- The application, not Ollama, is the source of truth for story state.
- Recommendations are **soft hints**, not hard directives:
- The model may diverge from a recommendation if it produces a better story.
- The backend does not enforce recommendation compliance.
### 6. Implementation Notes (Informative)
#### 6.1 Backend Technology Stack
- **Language & runtime**
- Java 21 (LTS).
- **Framework**
- Latest stable Spring Boot (3.x line at time of implementation).
- **Build & dependency management**
- Maven with:
- Spring Boot starter parent (for dependency management).
- OpenAPI / Swagger codegen Maven plugin.
- Lombok.
- JUnit 5 and Mockito for testing.
#### 6.2 Project Structure (Suggested)
- `roleplay-backend` (Spring Boot application)
- `src/main/java/.../api` -- generated API interfaces and DTOs (from OpenAPI).
- `src/main/java/.../controller` -- thin controllers implementing generated interfaces.
- `src/main/java/.../service` -- business logic (sessions, turns, prompts).
- `src/main/java/.../model` -- domain model (session, characters, situation, suggestions).
- `src/main/java/.../ollama` -- Ollama client (HTTP integration).
- `src/main/java/.../config` -- configuration (e.g. Ollama base URL, model defaults).
- `src/test/java/...` -- unit and integration tests.
#### 6.3 REST API and OpenAPI
- The REST API of the backend is **defined via an OpenAPI specification** (e.g. `roleplay-public-v1.yml`).
- Contains endpoints such as:
- `GET /models` -- list available Ollama models.
- `POST /sessions` -- create a new role play session.
- `GET /sessions/{id}` -- fetch session state.
- `POST /sessions/{id}/turns` -- submit a user action and get the next turn.
- Request schemas:
- `TurnRequest`: contains `user_action` (type + content + optional suggestion ID) and optional `recommendation` (desired_tone, preferred_direction, focus_characters).
- Response schemas:
- `TurnResponse`: contains `narrative` (string from Call 1), `responses` (per-character actions from Call 2), `suggestions` (from Call 2), and `updated_state_summary` (optional, for debugging/transparency).
- Other domain DTOs: `Session`, `Character`, `Situation`, `Suggestion`, `Recommendation`.
- **Code generation**
- Maven OpenAPI codegen plugin generates:
- Java interfaces for controllers (e.g. `SessionsApi`).
- DTO classes mirroring OpenAPI schemas.
- Application code:
- Implements generated interfaces in Spring `@RestController` classes.
- Maps DTOs <-> domain model where needed.
#### 6.4 Spring Boot Application Design
- **Application entry point**
- `@SpringBootApplication` main class starting the HTTP server.
- **Controllers**
- Implement generated API interfaces.
- Are kept thin and delegate to services:
- `ModelService` (wrapping Ollama `/api/tags`).
- `SessionService` (create/load/update sessions).
- `TurnService` (execute a role play turn via the two-call pattern).
- **Services**
- `PromptBuilderService`:
- Builds the structured JSON context object from session state, user action, and recommendation.
- Produces two system prompts per turn: one for Call 1 (narrative) and one for Call 2 (state extraction).
- `TurnService`:
- Orchestrates the two-call flow: Call 1 (narrative) -> Call 2 (state update) -> merge -> store.
- Handles errors (e.g. invalid JSON from Call 2: retry or fall back).
- `StateMergerService`:
- Applies diff-style updates from the Call 2 response to the stored session state.
- Appends `new_timeline_entries`, merges `open_threads_changes`, updates character fields.
- **Ollama client**
- Implemented as a Spring `@Service` or component using:
- `WebClient` (preferred) or `RestTemplate`.
- Configurable base URL (default `http://localhost:11434`), model default, and timeouts.
- Supports an optional `format` parameter on `/api/chat` calls (`null` for Call 1, `"json"` for Call 2).
- **Response handling**
- Call 1: response body is used as-is (plain text narrative).
- Call 2: response body is deserialized directly into a `StateUpdateResponse` POJO via Jackson. No regex or tag parsing required.
#### 6.5 Lombok Usage
- Lombok is used to reduce boilerplate in domain and DTO classes:
- `@Data` or `@Getter`/`@Setter` for simple POJOs.
- `@Builder` for objects that are convenient to construct fluently (e.g. prompts).
- `@AllArgsConstructor` / `@NoArgsConstructor` where needed (e.g. for Jackson).
- Guidelines:
- Keep domain models immutable where reasonable (`@Builder` + `@Getter` + `final` fields).
- Use explicit methods for logic; use Lombok only for structural boilerplate.
#### 6.6 Testing Strategy (JUnit 5, Mockito)
- **Unit tests**
- Written with JUnit 5 (Jupiter).
- Mockito for mocking dependencies (e.g. Ollama client, repositories).
- Test coverage focus:
- `PromptBuilderService`: ensure structured JSON context is correctly assembled.
- `StateMergerService`: ensure diff-style updates are applied correctly.
- `TurnService`: ensure two-call orchestration and error handling work.
- Business rules (e.g. suggestion mapping, state transitions).
- **Integration tests**
- Spring Boot test slices (e.g. `@SpringBootTest`).
- Mock Ollama using:
- MockWebServer (or WireMock) or a simple test double for the Ollama client.
- Validate REST endpoints from OpenAPI behave as specified (inputs, outputs, HTTP codes).
#### 6.7 State Storage and Frontend (Informative)
- **State storage**
- Can be in-memory (for early prototypes) or persisted in a database.
- `turn_history` may store:
- Raw user input (action + recommendation).
- Model outputs (narrative from Call 1, JSON state from Call 2).
- A derived summary to control context size.
- **Frontend**
- Renders:
- Chat-style view (user/AI messages).
- Rich narrative layout if desired.
- Character cards (with current mood, relationships, etc.).
- Suggestions as buttons/chips.
- Optional recommendation input (tone selector, free-text direction, character focus picker).
- **Extensibility**
- Additional features (experience points, branching save points, etc.) can be built on top of this core structure without changing the Ollama interaction pattern.