PtcRunner
View SourceBuild 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.00Try 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/pcallsexecute 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:
defpersists 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"}]
endDocumentation
Guides
- Getting Started - Build your first SubAgent
- Core Concepts - Context, memory, and the firewall convention
- Patterns - Chaining, orchestration, and composition
- Testing - Mocking LLMs and integration testing
- Troubleshooting - Common issues and solutions
Reference
- Signature Syntax - Input/output type contracts
- PTC-Lisp Specification - The language SubAgents write
- Benchmark Evaluation - LLM accuracy by model
Interactive
mix ptc.repl- Interactive REPL for testing PTC-Lisp expressions- Playground Livebook - Try PTC-Lisp interactively
- LLM Agent Livebook - Build an agent end-to-end
- Examples - Runnable example applications
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 #=> 3Programs 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