diff --git a/src/storied/sandbox.py b/src/storied/sandbox.py index 13bb335..aaff819 100644 --- a/src/storied/sandbox.py +++ b/src/storied/sandbox.py @@ -27,7 +27,7 @@ _LIMITS = pydantic_monty.ResourceLimits( _TOOL_FUNCTIONS: dict[str, str] = {} -def _roll_host(notation: str) -> dict[str, Any]: +def _roll_host(notation: str, reason: str | None = None) -> dict[str, Any]: """Host function: roll dice and return the result dict.""" return dice_roll(notation).to_dict() @@ -65,10 +65,23 @@ def _build_host_functions( return fns +def _sig(fn: Callable[..., Any], exclude: set[str], ret: str) -> str: + """Format a function signature with return type, excluding internal params.""" + sig = inspect.signature(fn, eval_str=True) + params = [p for p in sig.parameters.values() if p.name not in exclude] + param_str = ", ".join(str(p) for p in params) + return f"{fn.__name__}({param_str}) -> {ret}" + + def build_tool_signatures() -> str: - """Build human-readable function signatures for all DM tools.""" + """Build human-readable function signatures for all DM tools. + + Signatures reflect what's actually callable in the sandbox: + - roll() comes from _roll_host (returns dict) + - all other tools go through execute_tool (return str) + """ from storied.tools import ( - roll, recall, establish, mark, note_discovery, + recall, establish, mark, note_discovery, set_scene, update_character, create_character, tune, end_session, ) from storied.initiative import ( @@ -76,19 +89,17 @@ def build_tool_signatures() -> str: damage, heal, condition, end_initiative, ) - all_fns = [ - roll, recall, establish, mark, note_discovery, set_scene, + ctx_params = {"ctx", "tracker"} + str_fns = [ + recall, establish, mark, note_discovery, set_scene, update_character, create_character, tune, end_session, enter_initiative, next_turn, add_combatant, remove_combatant, damage, heal, condition, end_initiative, ] - lines: list[str] = [] - for fn in all_fns: - sig = inspect.signature(fn) - params = [p for p in sig.parameters.values() if p.name not in ("ctx", "tracker")] - param_str = ", ".join(str(p) for p in params) - lines.append(f"{fn.__name__}({param_str})") + lines: list[str] = [_sig(_roll_host, set(), "dict").replace("_roll_host", "roll")] + for fn in str_fns: + lines.append(_sig(fn, ctx_params, "str")) return "\n".join(lines) diff --git a/src/storied/tools.py b/src/storied/tools.py index 1133936..2ff3dc9 100644 --- a/src/storied/tools.py +++ b/src/storied/tools.py @@ -993,13 +993,15 @@ TOOL_DEFINITIONS = [ "name": "run_code", "description": ( "Run Python code in a secure sandbox. Use for calculations, random " - "generation, data formatting, or any computation the narrative needs. " - "All your tools are available as functions: recall(query=...), " - "establish(entity_type=..., name=...), roll('2d6+3'), etc. " - "No file/network access. Supported: variables, functions, loops, " - "conditionals, comprehensions, f-strings, plus re, json, datetime, " - "math, random from the standard library. No classes or imports beyond " - "builtins. Errors are returned as text — recover gracefully.\n\n" + "generation, data formatting, or any computation the narrative needs.\n\n" + "All your DM tools are callable as functions (see signatures below). " + "Most return a str with the result. The exception is roll(), which " + "returns a dict with keys: notation, rolls, kept, modifier, total — " + "use roll('2d6+3')['total'] for math, or index into rolls/kept for " + "individual dice. Use roll() for all randomness (no random module).\n\n" + "Language: variables, functions, loops, conditionals, comprehensions, " + "f-strings. Stdlib: re, json, datetime, math. No classes, no other " + "imports, no file/network access. Errors return as text.\n\n" "Available functions:\n{tool_signatures}" ), "input_schema": { diff --git a/tests/test_sandbox.py b/tests/test_sandbox.py index 5cb116e..9e402f1 100644 --- a/tests/test_sandbox.py +++ b/tests/test_sandbox.py @@ -53,6 +53,16 @@ class TestHostFunctions: assert "total=" in result + def test_roll_with_reason_positional(self): + result = execute('r = roll("1d6", "picking a name")\nr["total"]') + + assert result + + def test_roll_with_reason_keyword(self): + result = execute('r = roll(notation="1d6", reason="picking")\nr["total"]') + + assert result + def test_custom_host_function(self): result = execute( "double(5)",