PtcRunner.Lisp (PtcRunner v0.5.1)

View Source

Execute PTC programs written in Lisp DSL (Clojure subset).

PTC-Lisp enables LLMs to write safe programs that orchestrate tools and transform data. Unlike raw code execution (Python, JavaScript), PTC-Lisp provides safety by design: no filesystem/network access, no unbounded recursion, and deterministic execution in isolated BEAM processes with resource limits.

See the PTC-Lisp Specification for the complete language reference.

Tool Registration

Tools are functions that receive a map of arguments and return results. Note: tool names use kebab-case in Lisp (e.g., "get-user" not "get_user"):

tools = %{
  "get-user" => fn %{"id" => id} -> MyApp.Users.get(id) end,
  "search" => fn %{"query" => q} -> MyApp.Search.run(q) end
}

PtcRunner.Lisp.run(~S|(tool/get-user {:id 123})|, tools: tools)

Contract:

  • Receives: map() of arguments (may be empty %{})
  • Returns: Any Elixir term (maps, lists, primitives)
  • Should not raise (return {:error, reason} for errors)

Summary

Functions

Format an error tuple into a human-readable string.

Run a PTC-Lisp program.

Functions

format_error(other)

@spec format_error(term()) :: String.t()

Format an error tuple into a human-readable string.

Useful for displaying errors to users or feeding back to LLMs for retry.

Examples

iex> PtcRunner.Lisp.format_error({:parse_error, "unexpected token"})
"Parse error: unexpected token"

iex> PtcRunner.Lisp.format_error({:eval_error, "undefined variable: x"})
"Eval error: undefined variable: x"

run(source, opts \\ [])

@spec run(
  String.t(),
  keyword()
) :: {:ok, PtcRunner.Step.t()} | {:error, PtcRunner.Step.t()}

Run a PTC-Lisp program.

Parameters

  • source: PTC-Lisp source code as a string
  • opts: Keyword list of options
    • :context - Initial context map (default: %{})
    • :memory - Initial memory map (default: %{})
    • :tools - Map of tool names to functions (default: %{})
    • :signature - Optional signature string for return value validation
    • :float_precision - Number of decimal places for floats in result (default: nil = full precision)
    • :timeout - Timeout in milliseconds (default: 1000)
    • :max_heap - Max heap size in words (default: 1_250_000)
    • :max_symbols - Max unique symbols/keywords allowed (default: 10_000)
    • :max_print_length - Max characters per println call (default: 2000)
    • :filter_context - Filter context to only include accessed data keys (default: true)

Return Value

On success, returns:

  • {:ok, Step.t()} with:
    • step.return: The value returned to the caller
    • step.memory: Complete memory state after execution
    • step.usage: Execution metrics (duration_ms, memory_bytes)

On error, returns:

  • {:error, Step.t()} with:
    • step.fail.reason: Error reason atom
    • step.fail.message: Human-readable error description
    • step.memory: Memory state at time of error

Memory Contract

The memory contract is applied only at the top level (via apply_memory_contract/3):

  • If result is not a map: step.return = value, no memory update
  • If result is a map without :return: merges map into memory, returns map as step.return
  • If result is a map with :return: merges remaining keys into memory, returns :return value as step.return

Related modules:

Float Precision

When :float_precision is set, all floats in the result are rounded to that many decimal places. This is useful for LLM-facing applications where excessive precision wastes tokens.

# Full precision (default)
{:ok, step} = PtcRunner.Lisp.run("(/ 10 3)")
step.return
#=> 3.3333333333333335

# Rounded to 2 decimals
{:ok, step} = PtcRunner.Lisp.run("(/ 10 3)", float_precision: 2)
step.return
#=> 3.33

Resource Limits

Lisp programs execute with configurable timeout and memory limits:

PtcRunner.Lisp.run(source, timeout: 5000, max_heap: 5_000_000)

Exceeding limits returns an error:

  • {:error, {:timeout, ms}} - execution exceeded timeout
  • {:error, {:memory_exceeded, bytes}} - heap limit exceeded

Context Filtering

By default, PTC-Lisp performs static analysis to identify which data/xxx keys are accessed by a program, then filters the context to only include those datasets. This significantly reduces memory pressure when the context contains large datasets that aren't used.

# Only products is loaded into the sandbox, orders/employees are filtered out
ctx = %{"products" => large_list, "orders" => large_list, "employees" => large_list}
PtcRunner.Lisp.run("(count data/products)", context: ctx)

Scalar context values (strings, numbers, nil) are always preserved as they typically represent metadata like prompts or configuration.

Disable filtering if you need all context available (e.g., for dynamic data access):

PtcRunner.Lisp.run(source, context: ctx, filter_context: false)

See PtcRunner.Lisp.DataKeys for the static analysis implementation.