A 5e storytelling engine with an LLM DM
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add character creation, session management, and DM improvements

- Character creation flow: when no character exists, the CLI launches
into an AI-guided character creation conversation that walks the
player through building their character

- Session saving on quit: added end_session tool that the DM calls
when the player wants to stop playing. Saves current situation and
open threads. CLI also catches Ctrl+C/Ctrl+D and asks DM to save.

- Session recap on start: CLI sends [Session starting] signal, DM
responds with a brief "previously on..." to re-orient the player

- Reset command: `storied reset` clears player/world state for fresh
starts

- DM prompt refinements:
- Narrative rhythm guidance (slower/epic vs faster/beat-by-beat)
- Balance "yes, and..." with world constraints
- Ending sessions gracefully

- UI polish: dim blue rule lines above prompt and before DM response

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+547 -25
+45
prompts/character-creation.md
··· 1 + You are helping a player create a new D&D 5e character for a solo adventure. 2 + 3 + ## Your Role 4 + 5 + Guide the player through character creation conversationally. Don't present it as a form - have a natural back-and-forth where you learn about who they want to play. 6 + 7 + ## The Flow 8 + 9 + Start by asking what kind of hero they imagine. Let their answer guide you: 10 + 11 + - If they have a clear concept ("a sneaky halfling thief"), help them build toward it 12 + - If they're unsure, ask questions to discover what appeals to them 13 + - If they want to explore options, briefly describe what's available 14 + 15 + ## Key Decisions 16 + 17 + Work through these naturally (not necessarily in order): 18 + 19 + 1. **Concept**: Who is this person? What's their deal? 20 + 2. **Race**: Human, Elf, Dwarf, Halfling, etc. Look up races for traits. 21 + 3. **Class**: Fighter, Wizard, Rogue, etc. Look up classes for features. 22 + 4. **Background**: Acolyte, Criminal, Soldier, etc. Provides skills and flavor. 23 + 5. **Ability Scores**: Roll 4d6kh3 six times, let them assign scores. 24 + 6. **Starting Equipment**: Based on class and background. 25 + 7. **Details**: Name, personality, bonds, flaws - whatever feels right. 26 + 27 + ## Ability Scores 28 + 29 + Roll ability scores using the standard method (4d6, drop lowest, six times). Let the player assign the results to abilities as they choose based on their concept. 30 + 31 + ## Looking Things Up 32 + 33 + Use `lookup_rule` freely to check: 34 + - Racial traits (darkvision, resistances, etc.) 35 + - Class features (hit dice, proficiencies, starting abilities) 36 + - Background features (skills, tools, equipment) 37 + - Spell lists if they're a caster 38 + 39 + Get the details right - this character will be with them for a while. 40 + 41 + ## Finalizing 42 + 43 + Once you have everything, call `create_character` with the full character data. This saves their character sheet and you can transition to the adventure. 44 + 45 + Don't rush. Character creation is part of the fun. Let them explore, change their mind, and discover who they want to play.
+55 -2
prompts/dm-system.md
··· 1 1 You are an expert D&D 5e Dungeon Master running a solo adventure. 2 2 3 + ## Starting a Session 4 + 5 + When you see `[Session starting]`, begin with a brief "previously on..." recap: 6 + 7 + - Remind them where they are and what's happening 8 + - Mention any open threads or objectives 9 + - Set the scene to re-immerse them in the moment 10 + 11 + Keep it short - 2-4 sentences. Then smoothly transition into the current situation and invite action. The session state and campaign log below have everything you need. 12 + 13 + If this is a brand new adventure (no session state exists), skip the recap and simply begin the story with an opening scene. 14 + 3 15 ## Output Format 4 16 5 17 You're running in a terminal with markdown support. Use formatting to enhance readability: ··· 7 19 - *Italics* for character speech or internal thoughts 8 20 - Horizontal rules (---) to separate scenes 9 21 10 - Keep responses focused and paced well - this is interactive fiction, not a novel. End on moments that invite player action. 22 + End on moments that invite player action. 23 + 24 + ## Narrative Rhythm 25 + 26 + Vary your storytelling pace to match the moment: 27 + 28 + **Slower, more epic** - For dramatic reveals, tense confrontations, or emotional beats: 29 + - Longer, more atmospheric descriptions 30 + - Let tension build before resolution 31 + - Include sensory details and internal reactions 32 + - Make skill checks feel like meaningful moments in the story 33 + 34 + **Faster, beat-by-beat** - For action sequences, rapid exploration, or when momentum matters: 35 + - Shorter, punchier responses 36 + - Present choices quickly 37 + - Keep the energy moving 38 + - Roll dice freely without belaboring outcomes 39 + 40 + Read the player's energy. If they're giving short, action-oriented inputs, match that tempo. If they're exploring or engaging deeply with a scene, slow down and breathe life into it. 41 + 42 + Not every encounter needs the same weight. A routine check at the city gates is different from standing before the dragon's lair. 11 43 12 44 ## Core Principle: Real Mechanics 13 45 ··· 108 140 109 141 - Ask what the player wants to attempt, then determine if a roll is needed 110 142 - Failures create complications, not dead ends 111 - - The world is reactive and consistent 143 + 144 + **Balance "yes, and..." with world integrity.** Sometimes the answer is "yes, and here's what happens." Sometimes it's "you can try, but..." with real consequences. And occasionally it's "that won't work because..." when the world's logic demands it. 145 + 146 + The goal isn't to block the player, but to make the world feel real. A locked door stays locked without the key or a good roll. The guard captain won't be bribed with 5 gold. The dragon doesn't negotiate with level 1 adventurers. 147 + 148 + When you do push back, make it interesting - offer alternatives, hint at other approaches, or let failure open unexpected doors. The constraint itself becomes part of the story. 112 149 113 150 ## Character Management 114 151 ··· 161 198 - `[[The Rusty Anchor]]` - links to the location file 162 199 163 200 This helps load relevant context automatically when resuming sessions. 201 + 202 + ## Ending Sessions 203 + 204 + When the player indicates they want to stop playing (saying "quit", "exit", "I need to go", "let's stop here", etc.), use the `end_session` tool: 205 + 206 + 1. Give a brief, satisfying farewell that feels like a natural pause 207 + 2. Call `end_session` with a situation summary for next time 208 + 3. Include any open threads the player is pursuing 209 + 210 + Example: 211 + ``` 212 + end_session( 213 + situation="Kira has just descended into the Gloomwater Caves, torch in hand, following the trail of missing villagers. She found drag marks in the mud leading deeper.", 214 + threads=["Find the missing villagers", "The merchant's mysterious cargo - 50gp reward"] 215 + ) 216 + ``` 164 217 165 218 ## Duration Guidelines 166 219
+76
src/storied/character.py
··· 150 150 return new_body 151 151 152 152 153 + def create_character( 154 + player_id: str, 155 + name: str, 156 + race: str, 157 + char_class: str, 158 + level: int, 159 + abilities: dict[str, int], 160 + hp_max: int, 161 + ac: int, 162 + background: str | None = None, 163 + speed: int = 30, 164 + gold: int = 0, 165 + equipment: list[str] | None = None, 166 + features: list[str] | None = None, 167 + proficiencies: str | None = None, 168 + backstory: str | None = None, 169 + base_path: Path | None = None, 170 + ) -> str: 171 + """Create a new character from scratch. 172 + 173 + Args: 174 + player_id: Player identifier 175 + name: Character name 176 + race: Character race (e.g., "Human", "Elf") 177 + char_class: Character class (e.g., "Fighter", "Wizard") 178 + level: Starting level (usually 1) 179 + abilities: Dict of ability scores {"strength": 15, "dexterity": 14, ...} 180 + hp_max: Maximum HP 181 + ac: Armor class 182 + background: Background (e.g., "Soldier", "Acolyte") 183 + speed: Movement speed in feet 184 + gold: Starting gold 185 + equipment: List of equipment items 186 + features: List of class/racial features 187 + proficiencies: Proficiency description 188 + backstory: Character backstory 189 + base_path: Base path for players directory 190 + 191 + Returns: 192 + Confirmation message 193 + """ 194 + # Build frontmatter 195 + data = { 196 + "name": name, 197 + "race": race, 198 + "class": char_class, 199 + "level": level, 200 + "background": background, 201 + "hp": {"current": hp_max, "max": hp_max}, 202 + "ac": ac, 203 + "speed": speed, 204 + "abilities": abilities, 205 + "gold": gold, 206 + } 207 + 208 + # Build body sections 209 + body_parts = [] 210 + 211 + if proficiencies: 212 + body_parts.append(f"## Proficiencies\n{proficiencies}") 213 + 214 + if features: 215 + body_parts.append("## Features\n" + "\n".join(f"- {f}" for f in features)) 216 + 217 + if equipment: 218 + body_parts.append("## Equipment\n" + "\n".join(f"- {e}" for e in equipment)) 219 + 220 + if backstory: 221 + body_parts.append(f"## Backstory\n{backstory}") 222 + 223 + data["body"] = "\n\n".join(body_parts) 224 + 225 + save_character(player_id, data, base_path) 226 + return f"Created character '{name}' - a level {level} {race} {char_class}!" 227 + 228 + 153 229 def format_character_context(data: dict) -> str: 154 230 """Format character data for inclusion in the system prompt. 155 231
+176 -22
src/storied/cli.py
··· 124 124 return 0 125 125 126 126 127 + def cmd_reset(args: argparse.Namespace) -> int: 128 + """Reset player and world state to start fresh.""" 129 + import shutil 130 + 131 + base_path = Path.cwd() 132 + player_id = args.player or "default" 133 + world_id = args.world or "default" 134 + 135 + player_dir = base_path / "players" / player_id 136 + world_dir = base_path / "worlds" / world_id 137 + 138 + # Check what exists 139 + has_player = player_dir.exists() 140 + has_world = world_dir.exists() 141 + 142 + if not has_player and not has_world: 143 + print("Nothing to reset.") 144 + return 0 145 + 146 + # Show what will be deleted 147 + print("This will delete:") 148 + if has_player: 149 + print(f" - {player_dir.relative_to(base_path)}/") 150 + if has_world: 151 + print(f" - {world_dir.relative_to(base_path)}/") 152 + print() 153 + 154 + if not args.force: 155 + confirm = input("Are you sure? [y/N] ") 156 + if confirm.lower() != "y": 157 + print("Cancelled.") 158 + return 0 159 + 160 + # Delete 161 + if has_player: 162 + shutil.rmtree(player_dir) 163 + print(f"Deleted {player_dir.relative_to(base_path)}/") 164 + if has_world: 165 + shutil.rmtree(world_dir) 166 + print(f"Deleted {world_dir.relative_to(base_path)}/") 167 + 168 + print("Reset complete. Ready for a fresh start!") 169 + return 0 170 + 171 + 127 172 def cmd_play(args: argparse.Namespace) -> int: 128 173 """Start an interactive DM session.""" 129 174 import atexit ··· 133 178 from rich.live import Live 134 179 from rich.markdown import Markdown 135 180 from rich.panel import Panel 181 + from rich.rule import Rule 136 182 from rich.text import Text 137 183 184 + from storied.character import load_character 138 185 from storied.engine import DMEngine 139 186 140 187 # Set up readline history ··· 148 195 149 196 console = Console() 150 197 world_id = args.world if args.world else "default" 198 + player_id = "default" 151 199 152 - console.print(Panel.fit( 153 - "[bold]Welcome to Storied![/bold]\n" 154 - "Type [cyan]quit[/cyan] or [cyan]exit[/cyan] to end the session.\n" 155 - "Type [cyan]/context[/cyan] to see token usage.", 156 - title="Storied", 157 - border_style="green", 158 - )) 200 + # Check if character exists 201 + character = load_character(player_id) 202 + creation_mode = character is None 203 + 204 + if creation_mode: 205 + console.print(Panel.fit( 206 + "[bold]Welcome to Storied![/bold]\n" 207 + "Let's create your character!", 208 + title="Character Creation", 209 + border_style="yellow", 210 + )) 211 + prompt_name = "character-creation" 212 + else: 213 + console.print(Panel.fit( 214 + "[bold]Welcome to Storied![/bold]\n" 215 + "Let the DM know when you're ready to quit.\n" 216 + "Type [cyan]/context[/cyan] to see token usage.", 217 + title="Storied", 218 + border_style="green", 219 + )) 220 + prompt_name = "dm-system" 221 + 159 222 console.print(f"[dim]World: {world_id}[/dim]") 160 223 console.print() 161 224 162 - engine = DMEngine(world_id=world_id) 225 + engine = DMEngine(world_id=world_id, player_id=player_id, prompt_name=prompt_name) 226 + 227 + # If in creation mode, start the conversation 228 + if creation_mode: 229 + console.print("[dim]The DM will guide you through character creation...[/dim]") 230 + console.print() 163 231 164 232 def build_display(parts: list[tuple[str, str]]) -> Group: 165 233 """Build display from ordered parts (type, content).""" ··· 177 245 prev_type = part_type 178 246 return Group(*renderables) 179 247 248 + # Kick off the conversation with appropriate first message 249 + if creation_mode: 250 + first_message = "Let's create a character!" 251 + else: 252 + first_message = "[Session starting]" 253 + 180 254 try: 181 255 while True: 182 - try: 183 - game_time = engine.get_current_time() 184 - action = input(f"[{game_time}] > ") 185 - except EOFError: 186 - print() 187 - break 188 - 189 - if not action.strip(): 190 - continue 256 + # Get player input (or use first_message to kick off creation) 257 + if first_message: 258 + action = first_message 259 + first_message = None 260 + else: 261 + try: 262 + console.print(Rule(style="dim blue")) 263 + if creation_mode: 264 + action = input("> ") 265 + else: 266 + game_time = engine.get_current_time() 267 + action = input(f"[{game_time}] > ") 268 + except EOFError: 269 + console.print() 270 + # Ask DM to save on EOF (Ctrl+D) - skip if in creation mode 271 + if not creation_mode: 272 + console.print("[dim]Saving session...[/dim]") 273 + try: 274 + save_msg = "I need to quit now. Please save the game." 275 + list(engine.stream_action(save_msg)) 276 + except Exception: 277 + pass 278 + console.print( 279 + "[yellow]Session saved. Farewell, adventurer![/yellow]" 280 + ) 281 + else: 282 + console.print("[yellow]Farewell![/yellow]") 283 + break 191 284 192 - if action.strip().lower() in ("quit", "exit"): 193 - console.print("[yellow]Farewell, adventurer![/yellow]") 194 - break 285 + if not action.strip(): 286 + continue 195 287 196 288 # Handle /context command 197 289 if action.strip().lower() == "/context": ··· 266 358 continue 267 359 268 360 try: 361 + console.print(Rule(style="dim blue")) 269 362 console.print() # Blank line before DM response 270 363 parts: list[tuple[str, str]] = [] # (type, content) in order 271 364 ··· 282 375 live.update(build_display(parts)) 283 376 284 377 console.print() 378 + 379 + # Check if session ended (player quit gracefully) 380 + if engine.session_ended: 381 + console.print( 382 + "[yellow]Session saved. Farewell, adventurer![/yellow]" 383 + ) 384 + break 385 + 386 + # Check if character was just created 387 + if creation_mode and load_character(player_id) is not None: 388 + console.print() 389 + console.print(Panel.fit( 390 + "[bold green]Character created![/bold green]\n" 391 + "Run [cyan]storied play[/cyan] again to begin your adventure.", 392 + border_style="green", 393 + )) 394 + break 395 + 285 396 except KeyboardInterrupt: 286 397 console.print("\n[red][Interrupted][/red]") 287 - continue 398 + # Ask DM to save on interrupt - skip if in creation mode 399 + if not creation_mode: 400 + console.print("[dim]Saving session...[/dim]") 401 + try: 402 + save_msg = "I need to quit now. Please save the game." 403 + list(engine.stream_action(save_msg)) 404 + except Exception: 405 + pass 406 + console.print( 407 + "[yellow]Session saved. Farewell, adventurer![/yellow]" 408 + ) 409 + else: 410 + console.print("[yellow]Farewell![/yellow]") 411 + break 288 412 289 413 except KeyboardInterrupt: 290 - console.print("\n[yellow]Farewell, adventurer![/yellow]") 414 + console.print() 415 + # Outer interrupt - skip save in creation mode 416 + if not creation_mode: 417 + console.print("[dim]Saving session...[/dim]") 418 + try: 419 + save_msg = "I need to quit now. Please save the game." 420 + list(engine.stream_action(save_msg)) 421 + except Exception: 422 + pass 423 + console.print( 424 + "[yellow]Session saved. Farewell, adventurer![/yellow]" 425 + ) 426 + else: 427 + console.print("[yellow]Farewell![/yellow]") 291 428 292 429 return 0 293 430 ··· 361 498 help="World ID to use for world-specific content", 362 499 ) 363 500 play_parser.set_defaults(func=cmd_play) 501 + 502 + # reset command 503 + reset_parser = subparsers.add_parser("reset", help="Reset player and world state") 504 + reset_parser.add_argument( 505 + "--world", "-w", 506 + help="World ID to reset (default: default)", 507 + ) 508 + reset_parser.add_argument( 509 + "--player", "-p", 510 + help="Player ID to reset (default: default)", 511 + ) 512 + reset_parser.add_argument( 513 + "--force", "-f", 514 + action="store_true", 515 + help="Skip confirmation prompt", 516 + ) 517 + reset_parser.set_defaults(func=cmd_reset) 364 518 365 519 return parser 366 520
+17 -1
src/storied/engine.py
··· 34 34 player_id: str = "default", 35 35 base_path: Path | None = None, 36 36 model: str = "claude-sonnet-4-20250514", 37 + prompt_name: str = "dm-system", 37 38 ): 38 39 """Initialize the DM engine. 39 40 ··· 42 43 player_id: Player identifier for character loading (default: "default") 43 44 base_path: Base path for content resolution (defaults to cwd) 44 45 model: Claude model to use 46 + prompt_name: System prompt to use (default: "dm-system", or "character-creation") 45 47 """ 46 48 self.client = anthropic.Anthropic() 47 49 self.model = model ··· 55 57 self.total_input_tokens: int = 0 56 58 self.total_output_tokens: int = 0 57 59 60 + # Session end flag (set when end_session tool is called) 61 + self.session_ended: bool = False 62 + 58 63 # Campaign log for time tracking 59 64 self._campaign_log = CampaignLog(self.player_id, self.base_path) 60 65 61 66 # Build system prompt with full context 62 - self._base_prompt = load_prompt("dm-system") 67 + self._prompt_name = prompt_name 68 + self._base_prompt = load_prompt(prompt_name) 63 69 self._context_parts: dict[str, str] = {} 64 70 self.system_prompt = self._base_prompt + "\n\n" + self._build_context() 65 71 ··· 318 324 yield f"\n[Checking world: {tool_use['input'].get('query', '?')}...]\n" 319 325 elif tool_use["name"] == "update_character": 320 326 yield "\n[Updating character sheet...]\n" 327 + elif tool_use["name"] == "create_character": 328 + name = tool_use["input"].get("name", "character") 329 + yield f"\n[Creating {name}...]\n" 321 330 elif tool_use["name"] == "update_session": 322 331 yield "\n[Updating session state...]\n" 323 332 elif tool_use["name"] == "save_to_world": ··· 327 336 event = tool_use["input"].get("event", "?") 328 337 duration = tool_use["input"].get("duration", "?") 329 338 yield f"\n[Logging: {event} ({duration})]...\n" 339 + elif tool_use["name"] == "end_session": 340 + yield "\n[Saving session...]...\n" 330 341 331 342 result = execute_tool( 332 343 tool_use["name"], ··· 336 347 base_path=self.base_path, 337 348 campaign_log=self._campaign_log, 338 349 ) 350 + 351 + # Check if session ended 352 + if result == "SESSION_ENDED": 353 + self.session_ended = True 354 + result = "Session saved. Farewell!" 339 355 340 356 # Show dice roll results immediately 341 357 if tool_use["name"] == "roll_dice":
+178
src/storied/tools.py
··· 8 8 9 9 import yaml 10 10 11 + from storied.character import create_character as char_create 11 12 from storied.character import update_character as char_update 12 13 from storied.content import ContentResolver 13 14 from storied.dice import roll as dice_roll ··· 157 158 return char_update(player_id, updates, base_path) 158 159 159 160 161 + def create_character( 162 + name: str, 163 + race: str, 164 + char_class: str, 165 + level: int, 166 + abilities: dict[str, int], 167 + hp_max: int, 168 + ac: int, 169 + background: str | None = None, 170 + speed: int = 30, 171 + gold: int = 0, 172 + equipment: list[str] | None = None, 173 + features: list[str] | None = None, 174 + proficiencies: str | None = None, 175 + backstory: str | None = None, 176 + player_id: str = "default", 177 + base_path: Path | None = None, 178 + ) -> str: 179 + """Create a new player character and save to disk. 180 + 181 + Call this when character creation is complete. Include all the mechanical 182 + details needed to play: abilities, HP, AC, equipment, and features. 183 + 184 + Args: 185 + name: Character name 186 + race: Race (e.g., "Human", "High Elf", "Hill Dwarf") 187 + char_class: Class (e.g., "Fighter", "Wizard", "Rogue") 188 + level: Starting level (usually 1) 189 + abilities: All six ability scores as a dict: 190 + {"strength": 15, "dexterity": 14, "constitution": 13, 191 + "intelligence": 12, "wisdom": 10, "charisma": 8} 192 + hp_max: Maximum hit points 193 + ac: Armor class 194 + background: Background (e.g., "Soldier", "Sage", "Criminal") 195 + speed: Movement speed in feet (default 30) 196 + gold: Starting gold pieces 197 + equipment: List of equipment items 198 + features: List of racial and class features 199 + proficiencies: Description of proficiencies (armor, weapons, tools, saves, skills) 200 + backstory: Character backstory and personality 201 + 202 + Returns: 203 + Confirmation message 204 + """ 205 + return char_create( 206 + player_id=player_id, 207 + name=name, 208 + race=race, 209 + char_class=char_class, 210 + level=level, 211 + abilities=abilities, 212 + hp_max=hp_max, 213 + ac=ac, 214 + background=background, 215 + speed=speed, 216 + gold=gold, 217 + equipment=equipment, 218 + features=features, 219 + proficiencies=proficiencies, 220 + backstory=backstory, 221 + base_path=base_path, 222 + ) 223 + 224 + 160 225 def update_session( 161 226 situation: str | None = None, 162 227 location: str | None = None, ··· 311 376 return f"Logged: {anchor} | {event} | {duration}" 312 377 313 378 379 + def end_session( 380 + situation: str, 381 + threads: list[str] | None = None, 382 + player_id: str = "default", 383 + base_path: Path | None = None, 384 + ) -> str: 385 + """End the current session, saving the game state for next time. 386 + 387 + Call this when the player indicates they want to stop playing. This saves 388 + the current situation so the next session can resume smoothly. 389 + 390 + Before calling, give a brief farewell and summary of what happened this session. 391 + 392 + Args: 393 + situation: Summary of the current state of affairs for the next session. 394 + Write as if briefing a DM who will pick up where you left off. 395 + threads: Open plot threads or objectives to carry forward 396 + player_id: Player identifier 397 + base_path: Base path for players directory 398 + 399 + Returns: 400 + Confirmation that session was saved 401 + """ 402 + updates = {"situation": situation} 403 + if threads is not None: 404 + updates["threads"] = threads 405 + 406 + session_update(player_id, updates, base_path) 407 + return "SESSION_ENDED" 408 + 409 + 314 410 # Tool definitions for the Anthropic API 315 411 TOOL_DEFINITIONS = [ 316 412 { ··· 399 495 }, 400 496 }, 401 497 { 498 + "name": "create_character", 499 + "description": create_character.__doc__, 500 + "input_schema": { 501 + "type": "object", 502 + "properties": { 503 + "name": {"type": "string", "description": "Character name"}, 504 + "race": {"type": "string", "description": "Race (e.g., 'Human', 'High Elf')"}, 505 + "char_class": {"type": "string", "description": "Class (e.g., 'Fighter', 'Wizard')"}, 506 + "level": {"type": "integer", "description": "Starting level (usually 1)"}, 507 + "abilities": { 508 + "type": "object", 509 + "description": "All six ability scores: strength, dexterity, constitution, intelligence, wisdom, charisma", 510 + }, 511 + "hp_max": {"type": "integer", "description": "Maximum hit points"}, 512 + "ac": {"type": "integer", "description": "Armor class"}, 513 + "background": {"type": "string", "description": "Background (e.g., 'Soldier', 'Sage')"}, 514 + "speed": {"type": "integer", "description": "Movement speed in feet"}, 515 + "gold": {"type": "integer", "description": "Starting gold pieces"}, 516 + "equipment": { 517 + "type": "array", 518 + "items": {"type": "string"}, 519 + "description": "List of equipment items", 520 + }, 521 + "features": { 522 + "type": "array", 523 + "items": {"type": "string"}, 524 + "description": "List of racial and class features", 525 + }, 526 + "proficiencies": {"type": "string", "description": "Proficiency description"}, 527 + "backstory": {"type": "string", "description": "Character backstory and personality"}, 528 + }, 529 + "required": ["name", "race", "char_class", "level", "abilities", "hp_max", "ac"], 530 + }, 531 + }, 532 + { 402 533 "name": "update_session", 403 534 "description": update_session.__doc__, 404 535 "input_schema": { ··· 481 612 "required": ["event", "duration"], 482 613 }, 483 614 }, 615 + { 616 + "name": "end_session", 617 + "description": end_session.__doc__, 618 + "input_schema": { 619 + "type": "object", 620 + "properties": { 621 + "situation": { 622 + "type": "string", 623 + "description": "Summary of current state for the next session", 624 + }, 625 + "threads": { 626 + "type": "array", 627 + "items": {"type": "string"}, 628 + "description": "Open plot threads or objectives to carry forward", 629 + }, 630 + }, 631 + "required": ["situation"], 632 + }, 633 + }, 484 634 ] 485 635 486 636 ··· 539 689 base_path=base_path, 540 690 ) 541 691 692 + elif tool_name == "create_character": 693 + return create_character( 694 + name=tool_input["name"], 695 + race=tool_input["race"], 696 + char_class=tool_input["char_class"], 697 + level=tool_input["level"], 698 + abilities=tool_input["abilities"], 699 + hp_max=tool_input["hp_max"], 700 + ac=tool_input["ac"], 701 + background=tool_input.get("background"), 702 + speed=tool_input.get("speed", 30), 703 + gold=tool_input.get("gold", 0), 704 + equipment=tool_input.get("equipment"), 705 + features=tool_input.get("features"), 706 + proficiencies=tool_input.get("proficiencies"), 707 + backstory=tool_input.get("backstory"), 708 + player_id=player_id, 709 + base_path=base_path, 710 + ) 711 + 542 712 elif tool_name == "update_session": 543 713 return update_session( 544 714 situation=tool_input.get("situation"), ··· 566 736 advance_time=tool_input.get("advance_time", True), 567 737 tags=tool_input.get("tags"), 568 738 campaign_log=campaign_log, 739 + player_id=player_id, 740 + base_path=base_path, 741 + ) 742 + 743 + elif tool_name == "end_session": 744 + return end_session( 745 + situation=tool_input["situation"], 746 + threads=tool_input.get("threads"), 569 747 player_id=player_id, 570 748 base_path=base_path, 571 749 )