Skip to content

andreasronge/ptc_runner

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1,857 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PtcRunner

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

PtcRunner gives LLM agents a fast, secure, stateful code mode.

PtcRunner is a 0.x library under active development. Expect bugs, API changes, and breaking behavior as the project evolves.

Models write small PTC-Lisp programs. PtcRunner runs them in a BEAM-native sandbox with process isolation, timeouts, heap limits, and controlled tool access. Sessions can keep definitions and intermediate results across calls, so an agent can work in a REPL-like loop without repeatedly reloading context or restarting a runtime.

Use it over MCP from any language or client, or embed it directly in Elixir.

Start Here

Use code mode over MCP

ptc_runner_mcp is a standalone MCP server for sandboxed code execution. It gives MCP-compatible clients and server-side agent runtimes a fast REPL-like code mode backed by PTC-Lisp. Agents can evaluate programs, keep session state, call approved tools, aggregate large results, and return compact computed answers.

Use this when you want code execution for an agent, but do not want generated code to have direct filesystem, network, package-install, or OS process access. The server is useful both for local AI clients such as Claude Desktop, Cursor, Cline, and Claude Code, and for non-Elixir agent applications that want a stable protocol boundary around the sandbox.

Compared with process-per-call Python or JavaScript execution, PtcRunner keeps the runtime warm, evaluates small programs inside lightweight BEAM processes, and can preserve session state across calls.

Start with the mcp_server README for install, setup, client config, and server deployment. See docs/mcp-server.md for the security model, sessions, and architecture.

Build agents in Elixir

Use the ptc_runner library when you want SubAgents, signatures, tools, memory, text mode, PTC-Lisp mode, tracing, composition, and compiled workflows directly inside an Elixir application.

Elixir Quick Start

# Runnable doctest — uses a mock LLM so it works without API access.
# In production, swap `mock_llm` for `PtcRunner.LLM.callback("haiku")`.
iex> mock_llm = fn _request ->
...>   {:ok, "(->> (tool/get_orders) (filter #(> % data/threshold)) (reduce +))"}
...> end
iex> {:ok, step} = PtcRunner.SubAgent.run(
...>   "Total value of orders over ${{threshold}}",
...>   tools: %{"get_orders" => fn _ -> [1500.0, 950.0, 50.0] end},
...>   context: %{threshold: 100},
...>   llm: mock_llm,
...>   max_turns: 1
...> )
iex> step.return
2450.0

The llm: option accepts any 1-arity function — for tests, pass an inline lambda like mock_llm above. There is no separate stub/mock helper. See the Testing guide for scripted callbacks and integration patterns.

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:

(->> (tool/get_orders)
     (filter #(> (: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. Tool results stay in memory — the LLM explores data through code, exposing only relevant findings. This scales to thousands of items without context limits and eliminates hallucinated counts.

Best suited for: Document analysis (agentic RAG), log analysis, data aggregation, multi-source joins — any task where raw data volume would overwhelm an LLM's context window.

Key Features

  • Secure code mode: PTC-Lisp executes generated programs in a BEAM-native sandbox with process isolation, timeouts, and heap limits
  • Two agent modes: PTC-Lisp for multi-turn agentic workflows with tools, or text mode for direct LLM responses with optional native tool calling
  • Signatures: Type contracts ({sentiment :string, score :float}) that validate outputs and drive auto-retry on mismatch
  • Transactional memory: def persists data across turns without bloating context
  • Composable SubAgents: Nest agents as tools with isolated state and turn budgets
  • Recursive agents (RLM): Agents call themselves via :self tools to subdivide large inputs
  • Ad-hoc LLM queries: llm-query calls an LLM from within PTC-Lisp with signature-validated responses
  • Observable: Telemetry spans for every turn, LLM call, and tool call with parent-child correlation. JSONL trace logs with Chrome DevTools flame chart export for debugging multi-agent flows (interactive Livebook)
  • Context compaction: Pressure-triggered trimming for long-running multi-turn agents — opt in with compaction: true to drop older turns once a turn or token threshold is hit
  • BEAM-native: Parallel tool calling (pmap/pcalls), process isolation with timeout and heap limits, fault tolerance
  • MCP server: Expose the sandbox as code mode over MCP for clients and server-side agents, with optional stateful sessions and aggregator mode for upstream MCP tools

Examples

Parallel tool calling - fetch data concurrently:

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

Ad-hoc LLM judgment from code - the LLM writes programs that call other LLMs, with typed responses and parallel execution:

;; LLM generates this - each llm-query runs in parallel via pmap
(pmap (fn [item]
        (tool/llm-query {:prompt "Rate urgency: {{desc}}"
                         :signature "{urgent :bool, reason :string}"
                         :desc (:description item)}))
      data/items)

The agent decides what to ask and how to structure the response — at runtime, from within the generated program. Enable with llm_query: true. See the LLM Agent Livebook for a full example.

Compile SubAgents - LLM writes the orchestration logic once, execute deterministically:

# Orchestrator with SubAgentTools + pure Elixir functions
{:ok, compiled} = SubAgent.compile(orchestrator, llm: my_llm)

# LLM generated: (loop [joke initial, i 1] (if (tool/check ...) (return ...) (recur ...)))

# Execute with zero orchestration cost - only child SubAgents call the LLM
compiled.execute.(%{topic: "cats"}, llm: my_llm)

See the Joke Workflow Livebook for a complete example.

Text Mode

Not every task needs PTC-Lisp. Text mode (output: :text) uses the LLM provider's native tool calling API — ideal for smaller models or straightforward tasks:

# Plain text — no signature, raw string response
{:ok, step} = SubAgent.run(
  "Summarize this article: {{text}}",
  context: %{text: article},
  output: :text,
  llm: my_llm
)
step.return  #=> "The article discusses..."

# Structured JSON — signature validates the response
{:ok, step} = SubAgent.run(
  "Classify the sentiment of: {{text}}",
  context: %{text: "I love this product!"},
  output: :text,
  signature: "() -> {sentiment :string, score :float}",
  llm: my_llm
)
step.return  #=> %{"sentiment" => "positive", "score" => 0.95}

Text mode also supports tools. Define tools as arity-1 functions that receive a map of arguments:

defmodule Calculator do
  @doc "Add two numbers"
  @spec add(%{a: integer(), b: integer()}) :: integer()
  def add(%{"a" => a, "b" => b}), do: a + b

  @doc "Multiply two numbers"
  @spec multiply(%{a: integer(), b: integer()}) :: integer()
  def multiply(%{"a" => a, "b" => b}), do: a * b
end

PtcRunner auto-extracts the @doc and @spec into tool descriptions and JSON Schema for the LLM provider's native tool calling API — just pass bare function references:

{:ok, step} = SubAgent.run(
  "What is (3 + 4) * 5?",
  output: :text,
  signature: "() -> {result :int}",
  tools: %{
    "add" => &Calculator.add/1,
    "multiply" => &Calculator.multiply/1
  },
  llm: my_llm
)
step.return["result"]  #=> 35

For full control (or anonymous functions), pass an explicit signature string instead. See the Text Mode guide for all four variants (plain text, JSON, tool+text, tool+JSON).

PTC-Lisp Transport (ptc_transport)

For output: :ptc_lisp agents, ptc_transport controls how the LLM ships its program. :content (default) parses a markdown-fenced PTC-Lisp block from the assistant message — one program, one deterministic orchestration, lower latency and cost in a single LLM turn. :tool_call (opt-in) exposes a single internal lisp_eval tool to the provider's native tool-calling API; the model can call it zero or more times before returning a final answer directly. App tools stay inside PTC-Lisp in both transports — only lisp_eval is exposed natively.

Transport Default? Use when
:content yes One PTC-Lisp program is enough. Lowest latency and cost.
:tool_call opt-in Native tool calling is materially more reliable than fenced-code parsing on your provider/model, or the workload genuinely needs iterative refinement across multiple program executions.

:tool_call turns one program into a ReAct-style loop: that's a tradeoff, not an upgrade. Pay for it deliberately. Models without native tool calling cannot use :tool_call — those runs surface as :llm_error, with no fallback. See the PTC-Lisp Transport guide for the full decision and a runnable walkthrough.

Signatures and JSON Schema

Signatures are compact type contracts that validate SubAgent inputs and outputs:

"(query :string, limit :int) -> {total :float, items [{id :int, name :string}]}"

Under the hood, PtcRunner converts signatures to JSON Schema in two places:

Where When Purpose
Tool definitions Text mode with tools Tool signatures → JSON Schema parameters sent to the LLM provider's native tool calling API
Structured output Text mode with complex return type Return signature → JSON Schema passed to the LLM callback for provider-specific structured output (e.g., OpenAI response_format)

In PTC-Lisp mode, signatures stay in their compact form — the LLM sees them in the prompt and PtcRunner validates the result directly. JSON Schema is only generated when interfacing with LLM provider APIs that require it.

Auto-extraction from @spec means you can define tools as regular Elixir functions and skip writing signatures by hand. For full control, pass an explicit signature string:

"search" => {&MyApp.search/2, signature: "(query :string, limit :int) -> [{id :int}]"}

See Signature Syntax for the full type reference.

Installation

def deps do
  [
    {:ptc_runner, "~> 0.11.0"},
    {:req_llm, "~> 1.8"}  # optional — enables built-in LLM adapter
  ]
end

With req_llm installed, create LLM callbacks with zero configuration:

llm = PtcRunner.LLM.callback("openrouter:anthropic/claude-haiku-4.5")

# or with prompt caching
llm = PtcRunner.LLM.callback("bedrock:haiku", cache: true)

PtcRunner.LLM.callback/2 routes by model prefix (openrouter:, bedrock:, anthropic:, ollama:, etc.) and handles structured output, tool calling, and prompt caching. See the LLM Setup guide for all providers, streaming, custom adapters, and framework integration.

Documentation

Guides

Reference

PTC-Lisp follows Clojure where that is safe and bounded. Intentional divergences favor sandbox safety and recoverable signal values for Clojure-named helpers; Java-named dot methods keep Java semantics.

Interactive

Trace Viewer

A built-in web UI for browsing execution traces with turn-by-turn drill-down:

mix ptc.viewer --trace-dir traces

See Observability Guide for details.

Low-Level API

For direct program execution without the agentic loop:

{:ok, step} = PtcRunner.Lisp.run(
  "(->> data/items (filter :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.

License

MIT

About

Secure sandbox runtime for Programmatic Tool Calling, Code Mode, and MCP aggregation. Run many concurrent LLM/tool clients safely while orchestrating tools and transforming data.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors