A 5e storytelling engine with an LLM DM
0
fork

Configure Feed

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

Fix roll() signature mismatch in sandbox and clean up run_code docs

The sandbox's roll() only accepted notation, but build_tool_signatures()
advertised the MCP version's signature which includes reason — so the DM
tried passing reason and got a TypeError. Now _roll_host accepts reason
too (ignores it, it's only for the CLI notification).

Also cleaned up the run_code tool description: dropped the false claim
that random is available (it's not in Monty), spelled out clearly that
roll() -> dict while all other tools -> str, and showed how to use the
dict keys for math.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+41 -18
+22 -11
src/storied/sandbox.py
··· 27 27 _TOOL_FUNCTIONS: dict[str, str] = {} 28 28 29 29 30 - def _roll_host(notation: str) -> dict[str, Any]: 30 + def _roll_host(notation: str, reason: str | None = None) -> dict[str, Any]: 31 31 """Host function: roll dice and return the result dict.""" 32 32 return dice_roll(notation).to_dict() 33 33 ··· 65 65 return fns 66 66 67 67 68 + def _sig(fn: Callable[..., Any], exclude: set[str], ret: str) -> str: 69 + """Format a function signature with return type, excluding internal params.""" 70 + sig = inspect.signature(fn, eval_str=True) 71 + params = [p for p in sig.parameters.values() if p.name not in exclude] 72 + param_str = ", ".join(str(p) for p in params) 73 + return f"{fn.__name__}({param_str}) -> {ret}" 74 + 75 + 68 76 def build_tool_signatures() -> str: 69 - """Build human-readable function signatures for all DM tools.""" 77 + """Build human-readable function signatures for all DM tools. 78 + 79 + Signatures reflect what's actually callable in the sandbox: 80 + - roll() comes from _roll_host (returns dict) 81 + - all other tools go through execute_tool (return str) 82 + """ 70 83 from storied.tools import ( 71 - roll, recall, establish, mark, note_discovery, 84 + recall, establish, mark, note_discovery, 72 85 set_scene, update_character, create_character, tune, end_session, 73 86 ) 74 87 from storied.initiative import ( ··· 76 89 damage, heal, condition, end_initiative, 77 90 ) 78 91 79 - all_fns = [ 80 - roll, recall, establish, mark, note_discovery, set_scene, 92 + ctx_params = {"ctx", "tracker"} 93 + str_fns = [ 94 + recall, establish, mark, note_discovery, set_scene, 81 95 update_character, create_character, tune, end_session, 82 96 enter_initiative, next_turn, add_combatant, remove_combatant, 83 97 damage, heal, condition, end_initiative, 84 98 ] 85 99 86 - lines: list[str] = [] 87 - for fn in all_fns: 88 - sig = inspect.signature(fn) 89 - params = [p for p in sig.parameters.values() if p.name not in ("ctx", "tracker")] 90 - param_str = ", ".join(str(p) for p in params) 91 - lines.append(f"{fn.__name__}({param_str})") 100 + lines: list[str] = [_sig(_roll_host, set(), "dict").replace("_roll_host", "roll")] 101 + for fn in str_fns: 102 + lines.append(_sig(fn, ctx_params, "str")) 92 103 93 104 return "\n".join(lines) 94 105
+9 -7
src/storied/tools.py
··· 993 993 "name": "run_code", 994 994 "description": ( 995 995 "Run Python code in a secure sandbox. Use for calculations, random " 996 - "generation, data formatting, or any computation the narrative needs. " 997 - "All your tools are available as functions: recall(query=...), " 998 - "establish(entity_type=..., name=...), roll('2d6+3'), etc. " 999 - "No file/network access. Supported: variables, functions, loops, " 1000 - "conditionals, comprehensions, f-strings, plus re, json, datetime, " 1001 - "math, random from the standard library. No classes or imports beyond " 1002 - "builtins. Errors are returned as text — recover gracefully.\n\n" 996 + "generation, data formatting, or any computation the narrative needs.\n\n" 997 + "All your DM tools are callable as functions (see signatures below). " 998 + "Most return a str with the result. The exception is roll(), which " 999 + "returns a dict with keys: notation, rolls, kept, modifier, total — " 1000 + "use roll('2d6+3')['total'] for math, or index into rolls/kept for " 1001 + "individual dice. Use roll() for all randomness (no random module).\n\n" 1002 + "Language: variables, functions, loops, conditionals, comprehensions, " 1003 + "f-strings. Stdlib: re, json, datetime, math. No classes, no other " 1004 + "imports, no file/network access. Errors return as text.\n\n" 1003 1005 "Available functions:\n{tool_signatures}" 1004 1006 ), 1005 1007 "input_schema": {
+10
tests/test_sandbox.py
··· 53 53 54 54 assert "total=" in result 55 55 56 + def test_roll_with_reason_positional(self): 57 + result = execute('r = roll("1d6", "picking a name")\nr["total"]') 58 + 59 + assert result 60 + 61 + def test_roll_with_reason_keyword(self): 62 + result = execute('r = roll(notation="1d6", reason="picking")\nr["total"]') 63 + 64 + assert result 65 + 56 66 def test_custom_host_function(self): 57 67 result = execute( 58 68 "double(5)",