Skip to content

Commit e8ccef8

Browse files
andreasrongegithub-actions[bot]claude
authored
feat: Validate tool function arities at registration time (#42) (#68)
* feat: Validate tool function arities at registration time Move tool function arity validation from call-time to registration-time (when tools are passed to PtcRunner.run/2). This provides earlier error detection, better error messages, and fail-fast behavior for library users. Changes: - Add validate_tools/1 private function in PtcRunner to check all tools have arity 1 at registration time - Update run/2 to validate tools before creating context - Return {:error, {:validation_error, msg}} for invalid tools - Error message identifies which tools have wrong arity - Update existing test at line 3214 to expect validation_error - Add 6 new tests covering: - Arity-0 and arity-2 function detection - Non-function value detection - Multiple invalid tools reporting - Valid arity-1 functions passing validation - Empty tools map handling Fixes #42 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: Remove dead code in operations.ex Remove unreachable catch-all branch that checked for non-arity-1 functions. Since tools are now validated at registration time via validate_tools/1, all tools in the registry are guaranteed to be arity-1 functions. Changes: - Remove guard clause 'when is_function(tool_fn, 1)' from pattern match - Remove catch-all '_tool_fn' branch that is now unreachable - Simplify case statement to only handle nil (not found) and tool_fn (execute) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent fbdaea7 commit e8ccef8

File tree

3 files changed

+95
-12
lines changed

3 files changed

+95
-12
lines changed

lib/ptc_runner.ex

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,17 @@ defmodule PtcRunner do
6767
:ok <- Validator.validate(ast) do
6868
context_map = Keyword.get(opts, :context, %{})
6969
tools = Keyword.get(opts, :tools, %{})
70-
context = Context.new(context_map, tools)
7170

72-
sandbox_opts = [
73-
timeout: Keyword.get(opts, :timeout, 1000),
74-
max_heap: Keyword.get(opts, :max_heap, 1_250_000)
75-
]
71+
with :ok <- validate_tools(tools) do
72+
context = Context.new(context_map, tools)
7673

77-
Sandbox.execute(ast, context, sandbox_opts)
74+
sandbox_opts = [
75+
timeout: Keyword.get(opts, :timeout, 1000),
76+
max_heap: Keyword.get(opts, :max_heap, 1_250_000)
77+
]
78+
79+
Sandbox.execute(ast, context, sandbox_opts)
80+
end
7881
else
7982
{:error, reason} -> {:error, reason}
8083
end
@@ -107,4 +110,21 @@ defmodule PtcRunner do
107110
{:error, reason} -> raise "PtcRunner error: #{inspect(reason)}"
108111
end
109112
end
113+
114+
defp validate_tools(tools) do
115+
invalid_tools =
116+
tools
117+
|> Enum.reject(fn {_name, fun} -> is_function(fun, 1) end)
118+
|> Enum.map(fn {name, _fun} -> name end)
119+
120+
case invalid_tools do
121+
[] ->
122+
:ok
123+
124+
names ->
125+
{:error,
126+
{:validation_error,
127+
"Tools must be functions with arity 1. Invalid: #{Enum.join(names, ", ")}"}}
128+
end
129+
end
110130
end

lib/ptc_runner/operations.ex

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ defmodule PtcRunner.Operations do
369369
nil ->
370370
{:error, {:execution_error, "Tool '#{tool_name}' not found"}}
371371

372-
tool_fn when is_function(tool_fn, 1) ->
372+
tool_fn ->
373373
try do
374374
case tool_fn.(args) do
375375
{:error, reason} ->
@@ -381,9 +381,6 @@ defmodule PtcRunner.Operations do
381381
rescue
382382
e -> {:error, {:execution_error, "Tool '#{tool_name}' raised: #{Exception.message(e)}"}}
383383
end
384-
385-
_tool_fn ->
386-
{:error, {:execution_error, "Tool '#{tool_name}' is not a function with arity 1"}}
387384
end
388385
end
389386

test/ptc_runner_test.exs

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3221,8 +3221,74 @@ defmodule PtcRunnerTest do
32213221
"tool": "wrong_arity"
32223222
}})
32233223

3224-
{:error, {:execution_error, msg}} = PtcRunner.run(program, tools: tools)
3225-
assert String.contains?(msg, "is not a function with arity 1")
3224+
{:error, {:validation_error, msg}} = PtcRunner.run(program, tools: tools)
3225+
assert String.contains?(msg, "Tools must be functions with arity 1")
3226+
assert String.contains?(msg, "wrong_arity")
3227+
end
3228+
3229+
test "tool validation catches arity-2 function" do
3230+
tools = %{
3231+
"bad_tool" => fn _a, _b -> "two args" end
3232+
}
3233+
3234+
program = ~s({"program": {"op": "literal", "value": 42}})
3235+
3236+
{:error, {:validation_error, msg}} = PtcRunner.run(program, tools: tools)
3237+
assert String.contains?(msg, "Tools must be functions with arity 1")
3238+
assert String.contains?(msg, "bad_tool")
3239+
end
3240+
3241+
test "tool validation catches non-function values" do
3242+
tools = %{
3243+
"not_a_function" => "string value"
3244+
}
3245+
3246+
program = ~s({"program": {"op": "literal", "value": 42}})
3247+
3248+
{:error, {:validation_error, msg}} = PtcRunner.run(program, tools: tools)
3249+
assert String.contains?(msg, "Tools must be functions with arity 1")
3250+
assert String.contains?(msg, "not_a_function")
3251+
end
3252+
3253+
test "tool validation reports multiple invalid tools" do
3254+
tools = %{
3255+
"tool1" => fn -> "zero args" end,
3256+
"tool2" => "not a function",
3257+
"tool3" => fn _a, _b -> "two args" end,
3258+
"valid_tool" => fn _args -> "valid" end
3259+
}
3260+
3261+
program = ~s({"program": {"op": "literal", "value": 42}})
3262+
3263+
{:error, {:validation_error, msg}} = PtcRunner.run(program, tools: tools)
3264+
assert String.contains?(msg, "Tools must be functions with arity 1")
3265+
# All three invalid tools should be mentioned
3266+
assert String.contains?(msg, "tool1")
3267+
assert String.contains?(msg, "tool2")
3268+
assert String.contains?(msg, "tool3")
3269+
end
3270+
3271+
test "tool validation passes with valid arity-1 functions" do
3272+
tools = %{
3273+
"add" => fn %{"a" => a, "b" => b} -> a + b end,
3274+
"multiply" => fn %{"x" => x, "y" => y} -> x * y end
3275+
}
3276+
3277+
program = ~s({"program": {
3278+
"op": "call",
3279+
"tool": "add",
3280+
"args": {"a": 2, "b": 3}
3281+
}})
3282+
3283+
{:ok, result, _metrics} = PtcRunner.run(program, tools: tools)
3284+
assert result == 5
3285+
end
3286+
3287+
test "tool validation passes with empty tools map" do
3288+
program = ~s({"program": {"op": "literal", "value": 42}})
3289+
3290+
{:ok, result, _metrics} = PtcRunner.run(program, tools: %{})
3291+
assert result == 42
32263292
end
32273293

32283294
test "missing tool field raises validation error" do

0 commit comments

Comments
 (0)