Packages

Secure BEAM sandbox runtime for LLM code mode and MCP aggregation. Run concurrent LLM/tool clients safely while agents orchestrate approved tools, call upstream MCP/OpenAPI servers, and transform data.

Current section

Files

Jump to
ptc_runner lib ptc_runner.ex
Raw

lib/ptc_runner.ex

defmodule PtcRunner do
@moduledoc """
A BEAM-native Programmatic Tool Calling (PTC) runner.
PtcRunner enables LLMs to write safe programs that orchestrate tools
and transform data inside a sandboxed environment.
## Examples
iex> program = ~s({"program": {"op": "literal", "value": 42}})
iex> {:ok, result, _metrics} = PtcRunner.run(program)
iex> result
42
"""
alias PtcRunner.Context
alias PtcRunner.Parser
alias PtcRunner.Sandbox
alias PtcRunner.Validator
@typedoc """
Execution metrics for a program run.
"""
@type metrics :: %{
duration_ms: integer(),
memory_bytes: integer()
}
@typedoc """
Error types returned by PtcRunner operations.
"""
@type error ::
{:parse_error, String.t()}
| {:validation_error, String.t()}
| {:execution_error, String.t()}
| {:timeout, non_neg_integer()}
| {:memory_exceeded, non_neg_integer()}
@doc """
Runs a PTC program and returns the result with metrics.
## Arguments
- program: JSON string or parsed map representing the program
- opts: Execution options
## Options
- `:context` - Map of pre-bound variables (default: `%{}`)
- `:tools` - Tool registry (default: `%{}`)
- `:timeout` - Timeout in milliseconds (default: 1000)
- `:max_heap` - Max heap size in words (default: 1_250_000)
## Returns
- `{:ok, result, metrics}` on success
- `{:error, reason}` on failure
## Examples
iex> {:ok, result, _metrics} = PtcRunner.run(~s({"program": {"op": "literal", "value": 42}}))
iex> result
42
"""
@spec run(String.t() | map(), keyword()) ::
{:ok, any(), metrics()} | {:error, error()}
def run(program, opts \\ []) do
with {:ok, ast} <- Parser.parse(program),
:ok <- Validator.validate(ast) do
context_map = Keyword.get(opts, :context, %{})
tools = Keyword.get(opts, :tools, %{})
with :ok <- validate_tools(tools) do
context = Context.new(context_map, tools)
sandbox_opts = [
timeout: Keyword.get(opts, :timeout, 1000),
max_heap: Keyword.get(opts, :max_heap, 1_250_000)
]
Sandbox.execute(ast, context, sandbox_opts)
end
else
{:error, reason} -> {:error, reason}
end
end
@doc """
Runs a PTC program, raising on error.
## Arguments
- program: JSON string or parsed map representing the program
- opts: Execution options (same as `run/2`)
## Returns
- The result value (metrics are discarded)
## Raises
- Raises an error if parsing, validation, or execution fails
## Examples
iex> result = PtcRunner.run!(~s({"program": {"op": "literal", "value": 42}}))
iex> result
42
"""
@spec run!(String.t() | map(), keyword()) :: any()
def run!(program, opts \\ []) do
case run(program, opts) do
{:ok, result, _metrics} -> result
{:error, reason} -> raise "PtcRunner error: #{inspect(reason)}"
end
end
@doc """
Formats an error into a human-readable message suitable for LLM feedback.
This function converts internal error tuples into clear, actionable messages
that help LLMs understand what went wrong and how to fix their programs.
## Examples
iex> PtcRunner.format_error({:parse_error, "unexpected token"})
"ParseError: unexpected token"
iex> PtcRunner.format_error({:validation_error, "unknown operation"})
"ValidationError: unknown operation"
"""
@spec format_error(error() | any()) :: String.t()
def format_error({:parse_error, msg}), do: "ParseError: #{msg}"
def format_error({:validation_error, msg}), do: "ValidationError: #{msg}"
def format_error({:timeout, ms}), do: "TimeoutError: execution exceeded #{ms}ms limit"
def format_error({:memory_exceeded, bytes}), do: "MemoryError: exceeded #{bytes} byte limit"
def format_error({:execution_error, msg}) when is_binary(msg) do
cond do
msg =~ "badmap" || msg =~ "expected a map" ->
extract_type_error(msg)
msg =~ "badkey" || msg =~ ~r/key .+ not found/ ->
extract_key_error(msg)
msg =~ "undefined_variable" ->
extract_undefined_var_error(msg)
msg =~ "badarith" ->
"ArithmeticError: invalid arithmetic operation (e.g., division by zero or wrong types)"
msg =~ "function_clause" ->
"TypeError: function received unexpected argument types"
true ->
# Extract first meaningful line for generic errors
msg
|> String.split("\n")
|> List.first()
|> String.slice(0, 200)
|> then(&"ExecutionError: #{&1}")
end
end
def format_error(other), do: "Error: #{inspect(other, limit: 5)}"
# Extract type error details from badmap errors
defp extract_type_error(msg) do
# Try to extract the value that was received instead of a map
cond do
# Match: expected a map, got:\n\n "value"
match = Regex.run(~r/expected a map, got:\s*\n?\s*"([^"]+)"/s, msg) ->
[_, value] = match
"TypeError: expected an object but got: \"#{String.slice(value, 0, 50)}\""
# Match: expected a map, got:\n\n value (non-quoted)
match = Regex.run(~r/expected a map, got:\s*\n?\s*([^\n"]+)/s, msg) ->
[_, value] = match
"TypeError: expected an object but got: #{String.trim(value) |> String.slice(0, 50)}"
# Match badmap tuple: {:badmap, "value"}
match = Regex.run(~r/badmap,\s*"([^"]+)"/, msg) ->
[_, value] = match
"TypeError: expected an object but got: \"#{String.slice(value, 0, 50)}\""
true ->
"TypeError: expected an object but got a different type"
end
end
# Extract key error details
defp extract_key_error(msg) do
cond do
# Match: key :atom not found
match = Regex.run(~r/key\s+:(\w+)\s+not found/, msg) ->
[_, key] = match
"KeyError: field '#{key}' not found in object"
# Match: key "string" not found
match = Regex.run(~r/key\s+"([^"]+)"\s+not found/, msg) ->
[_, key] = match
"KeyError: field '#{key}' not found in object"
true ->
"KeyError: field not found in object"
end
end
# Extract undefined variable details
defp extract_undefined_var_error(msg) do
case Regex.run(~r/undefined_variable.*[,{]?\s*"(\w+)"/, msg) do
[_, var] -> "UndefinedVariable: '#{var}' is not defined in context"
_ -> "UndefinedVariable: variable not found in context"
end
end
defp validate_tools(tools) do
invalid_tools =
tools
|> Enum.reject(fn {_name, fun} -> is_function(fun, 1) end)
|> Enum.map(fn {name, _fun} -> name end)
case invalid_tools do
[] ->
:ok
names ->
{:error,
{:validation_error,
"Tools must be functions with arity 1. Invalid: #{Enum.join(names, ", ")}"}}
end
end
end