PtcRunner

View Source

Hex.pm Docs CI Hex Downloads License GitHub Run in Livebook

Build LLM agents that write and execute programs. SubAgents combine the reasoning power of LLMs with the computational precision of a sandboxed interpreter.

Quick Start

# Conceptual example - see Getting Started guide for runnable code
{:ok, step} = PtcRunner.SubAgent.run(
  "What's the total value of orders over $100?",
  tools: %{"get_orders" => &MyApp.Orders.list/0},
  signature: "{total :float}",
  llm: my_llm
)

step.return.total  #=> 2450.00

Try it yourself: The Getting Started guide includes fully runnable examples you can copy-paste.

The SubAgent doesn't answer directly - it writes a program that computes the answer:

(->> (ctx/get_orders)
     (filter (where :amount > 100))
     (sum-by :amount))

This is Programmatic Tool Calling: instead of the LLM being the computer, it programs the computer.

Why PtcRunner?

LLMs as programmers, not computers. Most agent frameworks treat LLMs as the runtime. PtcRunner inverts this: LLMs generate programs that execute deterministically in a sandbox.

BEAM-Native Advantages

  • Parallel tool calling: pmap/pcalls execute I/O concurrently using lightweight BEAM processes
  • Process isolation: Each execution runs in a sandboxed process with timeout and heap limits
  • Fault tolerance: Crashes don't propagate; built-in supervision patterns

Safe Lisp DSL

  • LLM-friendly: Minimal syntax, easy to generate correctly
  • Safe by construction: No side effects, no system access, bounded iteration
  • Inspectable: Debug by examining generated programs

Unique Features

  • Context firewall: _ prefixed fields stay in BEAM memory, hidden from LLM prompts
  • Transactional memory: def persists data across turns without bloating context
  • Composable SubAgents: Nest agents as tools with isolated state and turn budgets
  • Type-driven retry: Signatures validate outputs; agents auto-correct on mismatch

Examples

Parallel tool calling - fetch data concurrently:

;; LLM generates this - executes in parallel automatically
(let [[user orders stats] (pcalls #(ctx/get_user {:id ctx/user_id})
                                   #(ctx/get_orders {:id ctx/user_id})
                                   #(ctx/get_stats {:id ctx/user_id}))]
  {:user user :order_count (count orders) :stats stats})

Context firewall - keep large data out of LLM prompts:

# The LLM sees: %{summary: "Found 3 urgent emails"}
# Elixir gets: %{summary: "...", _email_ids: [101, 102, 103]}
signature: "{summary :string, _email_ids [:int]}"

Compile SubAgents - LLM called once, execute many times:

# LLM derives the program once during compilation
{:ok, compiled} = SubAgent.compile(classifier_agent, llm: my_llm, sample: %{text: "example"})

# Execute without LLM calls - deterministic and fast
compiled.execute.(%{text: "new input"})  #=> %Step{return: %{category: "support"}}

Installation

def deps do
  [{:ptc_runner, "~> 0.4.1"}]
end

Documentation

Guides

Reference

Interactive

Low-Level API

For direct program execution without the agentic loop:

{:ok, step} = PtcRunner.Lisp.run(
  "(->> ctx/items (filter (where :active)) (count))",
  context: %{items: items}
)
step.return  #=> 3

Programs run in isolated BEAM processes with resource limits (1s timeout, 10MB heap).

See PtcRunner.Lisp module docs for options. A JSON DSL (PtcRunner.Json) is also available for schema-enforced execution.

License

MIT