A sandboxed language for AI agents whose tool calls run in parallel — automatically.
Give an agent a single run tool and it writes a small program instead of one
tool call per turn ("code mode"). Pingo executes that program: a tiny, pure
Scheme that starts with zero access to the world and reaches out only through
the tools you grant it. The part nobody else does — Pingo figures out which of
those tool calls are independent and runs them together, without the model
ever writing async or gather. It runs in-process: no container, no
subprocess, no server. Written in Zig.
;; The model wrote this to answer "which of these cities is hottest?".
;; It reads like straight-line code — Pingo runs it in two parallel waves.
(define cities (list "Paris" "Tokyo" "Lima"))
(define coords (map geocode cities)) ; 3 geocode calls — dispatched together
(define temps (map forecast coords)) ; each forecast waits only on its own
(apply max temps) ; coord, so all 3 fire together tooNo promises, no annotations, no gather. Two calls run at the same time whenever
their inputs don't depend on each other — that falls out of the data flow,
not out of the prompt.
- Parallelism for free. Independent tool calls go out as a batch and are
serviced concurrently; a call waiting on another's result parks and fires
itself the moment that result lands. Plain
(map f xs)fans out. The agent never asks for concurrency — the runtime reads it from the program. - Effects the runtime understands. Each tool declares an effect class —
pure,independent,resource,ordered,irreversible— so Pingo knows what may overlap, what may be retried, and what must never run early. An irreversible call (charge a card, send an email) is never dispatched speculatively. - Deterministic and replayable. The language has no mutation — everything is immutable — so a run depends only on its inputs and its tool results. Record every settled call and replay the whole program later, exactly, offline.
- Sandboxed by construction. No filesystem, no environment, no network, no ambient authority anywhere. Pure data goes in, pure data comes out; the tools are the only door.
zig build # builds ./zig-out/bin/pingo (Zig 0.16)
./zig-out/bin/pingo # a REPL
./zig-out/bin/pingo prog.scm # run a fileWatch the parallelism, with simulated tools on a virtual clock:
./zig-out/bin/pingo tests/examples/p1-fanout.scm \
--tool summarize:independent:100 --tool synthesize:independent:50 --trace
# ... 4 summarize calls dispatched together at t=0 ...
# virtual time: 150ms | sequential sum: 450ms | speedup: 3.00x--record run.trace / --replay run.trace capture and re-run a session;
--async swaps the virtual clock for a real libxev event loop (kqueue/io_uring)
so an independent batch overlaps in wall-clock time.
Pingo is a library first. The Python binding wraps it via ctypes and marshals
values to and from ordinary Python objects:
import asyncio
from pingo import Session, INDEPENDENT
async def geocode(city): ... # your real async tool
async def main():
async with Session() as s:
s.define_async("geocode", geocode, cls=INDEPENDENT)
# both calls are independent -> dispatched together -> run concurrently
return await s.run('(map geocode (list "Paris" "Tokyo"))')
asyncio.run(main())The binding, the CLI, and examples/code-mode (an LLM writing Pingo through
pydantic-ai) are all thin layers over one C library — libpingo, a small header
(include/pingo.h): feed a program, service the outstanding calls, resume. Build
a self-contained Python wheel with uv build (the compiled library is bundled);
see python/README.md.
A pure subset of R7RS-small plus a few R6RS/SRFI extensions: syntax-rules
hygienic macros, call/cc, dynamic-wind, records (define-record-type),
exceptions (guard/raise), list HOFs (map/fold-left/filter/…), and
SRFI-115 regex whose patterns are s-expressions. No mutation, by design — loops
and accumulation are recursion, do, map, and fold. Two engines evaluate
every program — a readable reference and a fast explicit-stack machine —
differentially tested against each other so they can't drift.
The capability boundary follows Monty's "no ambient authority" stance.
zig build test runs the suite; zig build conformance runs the vendored
R5RS/R7RS oracles.
Pingo's evaluation follows the Opportunistically Parallel Lambda Calculus (λᴼ) — a core calculus where a program reads sequentially but the runtime dispatches independent external calls in parallel, and is confluent: the order in which those calls complete never changes the result, so the parallel run is observably identical to running the program top to bottom. λᴼ, and the systems paper PopPy, target exactly this compound-AI / tool-calling setting, where LLM programs are "essentially pure" and most tool calls commute.
Pingo takes that model natively — pending values as placeholders for
in-flight calls, dataflow dispatch, sequential-semantics preservation — instead
of compiling to an intermediate calculus. It then goes further where λᴼ stops:
λᴼ assumes effects commute and leaves genuinely non-commutative effects out of
scope, whereas Pingo makes them explicit with effect classes
(resource/ordered/irreversible) — a stateful or irreversible tool is
ordered or held back, not reordered. And because the language forbids mutation,
every run is not just confluent but fully deterministic and replayable.
- λᴼ — Opportunistically Parallel Lambda Calculus — arXiv:2405.11361
- PopPy — Opportunistically Exploiting Parallelism in Python Compound AI Applications — arXiv:2605.18697
Zig 0.16