pinecall
The Pinecall voice-AI runtime: one Python distribution, two processes, on LiveKit.
pinecall-runtime gatewayis the control plane: the app protocol over WebSocket, the call log over SSE, the tokens, the routes, WhatsApp's webhook, the operator API.pinecall-runtime workeris the fleet: one livekit-agents worker, one process per call.
The public talks to an agent by web, WhatsApp and telephone. The agent itself is written with
the pinecall framework, in the agents repository; this runtime holds the log, the wire, the
tenants, the sessions and the judges, and never the conversation. How the pieces fit — and
exactly where LiveKit ends and this begins — is ARCHITECTURE.md.
Five minutes, on a laptop
docker compose -f infra/compose/dev.yml up -d livekit · sip · redis · postgres · tei
scripts/bootstrap uv sync, every extra and tool group
uv run pinecall-runtime migrate up the schema; a fresh database seeds the
default org, and `keys issue` mints its key
uv run pinecall-runtime gateway the control plane, on 8080
uv run pinecall-runtime worker dev the fleet that answers a call
uv run pinecall-runtime doctor every service and key, one line each
Then, from an example in the agents repository, pinecall run registers the agent and
pinecall chat talks to it. pinecall knowledge push ./knowledge/docs --base clinica-norte
puts the agent's files where its search tool reads from, and pinecall memory <contact> prints what a caller's calls taught the agent (memory forget erases it). Both are
tables in Postgres, and the vectors are whichever embedder EMBED_PROVIDER names — with no
embedder answering, a lookup finds nothing and the push says so.
EMBED_PROVIDER=tei who embeds: tei · perplexity · openrouter
EMBED_MODEL= unset: BAAI/bge-m3 · pplx-embed-context-v1-0.6b ·
perplexity/pplx-embed-v1-0.6b, by provider
EMBED_BASE_URL= unset: the provider's own door, and TEI_URL for TEI
PERPLEXITY_API_KEY= the two hosted ones. Perplexity's default model is
OPENROUTER_API_KEY= CONTEXTUAL: a chunk is embedded seeing its neighbours
TEI's CPU image has no arm64 build, so on an Apple Silicon laptop TEI cannot run at all and
EMBED_PROVIDER=perplexity with PERPLEXITY_API_KEY is how that machine retrieves — no
container, and a better base than bge-m3 gives. docs/decisions/retrieval.md says why, and what
push it again means when a box changes embedder. Development happens from the checkout, with uv:
scripts/format ruff format, then the fixable lint rules
scripts/lint ruff, pyright strict, mypy strict
scripts/test pytest -m "unit or postgres": no keys, no network
The wire is pinecall-protocol, generated in the protocol repository and resolved here as the
checkout beside this one (../protocol/python, see pyproject.toml).
Deploy: a box of your own
Two ways to run this on a server, and the first is the one we run ourselves.
1. A checkout and make deploy
The box is declared: infra/box/ holds cloud-init, the systemd units, the Quadlet containers
(LiveKit, SIP, Redis, Postgres), the Caddyfile, the firewall and a Makefile that is the manifest.
It runs on any Linux with systemd ≥ 254 and podman ≥ 4.9, on any provider — the box holds no
credential for the repository and is never told which cloud it is on.
1. the machine hand your provider infra/box/cloud-init.yaml as user-data, with the three
YOURS lines filled: your ssh public key, the domain, the SFU's public URL
2. which box runtime/deploy.local.mk, git-ignored:
BOX = deploy@203.0.113.7
DOMAIN = box.example.com
SSH_KEY = ~/.ssh/id_ed25519 # optional
3. make deploy from this directory. `scripts/console` (the agents repo's console bundled and
copied in as package data; needs pnpm and ../agents) · rsync the checkout · `make -C infra/box install` (the
packages, every unit and container file, the fence, the role) · `uv sync
--frozen` as the service user · restart, gateway first and the worker once
the gateway answers through Caddy · the doctor, last
4. your key minted on the box on first start, encrypted, printed nowhere — read it once:
sudo systemd-creds decrypt --name=PINECALL_OPERATOR_KEY \
/etc/credstore.encrypted/PINECALL_OPERATOR_KEY -
Every secret on the box is an encrypted systemd credential; there is no .env there. The vendors'
keys are yours to bring, and a deploy ends by knocking at every vendor with the key the box
holds — a dead one fails the deploy with its name on the screen, never a caller's first call.
printf '%s' "$ELEVENLABS_API_KEY" | make secret NAME=ELEVEN_API_KEY one secret, on stdin
make restart a credential is read at start
make doctor what the deploy runs last
make providers [DOES=tts] every vendor, and what each wants
make status · make logs UNIT=worker · make ssh
A second box. One machine is PINECALL_ROLE=all. To grow, the machine you have becomes the
hub (PINECALL_ROLE=hub in its /etc/pinecall/box.env: gateway and media plane, no worker)
and each new machine is a worker (PINECALL_ROLE=worker, LIVEKIT_URL and
PINECALL_GATEWAY_URL pointing at the hub, PINECALL_MAX_JOBS measured on it). The hub
copies a worker its credentials, then the worker is deployed like any box:
make worker-secrets WORKER=deploy@203.0.113.9
make deploy BOX=deploy@203.0.113.9 DOMAIN=box.example.com
The whole of it — the fence, the roles, the slots, the four traps a real box has, wiring a number — is infra/box/README.md.
2. The package, and a machine you build yourself
pinecall is a plain Python distribution with one entrypoint, for whoever already has a way to
run processes (Kubernetes, Nomad, Ansible) and wants ours out of the picture:
pip install pinecall the gateway
pip install pinecall[runtime] the gateway and the worker, on the five tuned vendors
pip install pinecall[runtime,providers] and the other forty livekit ships a plugin for
pinecall-runtime migrate up then gateway, and worker start, two long-running processes
providers is thin HTTP clients and is what a box installs; providers-big is the four that
bring a whole SDK with them — AWS, Azure Speech, Google Cloud, Speechmatics — and is a decision,
never a default. pinecall-runtime providers prints the whole table with what each vendor still
wants; a vendor whose plugin is missing is refused by name, with the one command that installs it.
The two processes read the environment .env.example documents — the same names on a box, where
they arrive as systemd credentials — and need a LiveKit server, a Postgres 17 with pgvector and
pg_textsearch, and Redis beside them. What runs them, restarts them and fences them is yours.
(Not on PyPI yet: the version and the tag are a person's call.)
The CLI
pinecall-runtime <group> <verb>. Eleven groups; every verb answers --help.
The two processes
| verb | what |
|---|---|
gateway [--host] [--port] [--reload] |
the control plane, on 8080 |
worker dev · worker start |
the fleet, with livekit's own flags passed through; each heartbeats to the gateway |
worker overflow |
the hub's one worker that is never full: a sentence and a call back when every other is |
worker talk |
one call served by THIS terminal — a @tool breakpoint lands where you typed |
worker download-files |
livekit's model files, ahead of the first call |
The box's operator
| verb | what |
|---|---|
migrate up [--post] · migrate status · migrate plan |
the schema, numbered SQL, applied in order. up says which database first, takes an advisory lock, and holds every migration to 5 s; a .post.sql is named and never run at startup, so --post is how an index on a big table gets built. status asks the database, plan touches nothing |
doctor |
keys present · keys answer · livekit · postgres · embedder · lk — one line each, and what is down first |
box secrets |
every secret a box makes for itself, once; run twice rotates nothing |
box secret <NAME> |
one secret you bring, from stdin, replaced in place |
fleet list · cordon · uncordon · loop |
the workers as the hub hears them, the graceful shrink, and the loop that keeps busy at the target over any cloud |
providers [--does llm|stt|tts] |
every vendor this build runs — forty-five — as a table: what each does, whether this box has its plugin and its key, the variable a key goes under, and every other word the vendor answers to. Reads the catalog and this process's settings; asks nothing of anybody, so it answers on a box that is down. Never a key |
orgs list · add · rm · quota · provider-key |
the tenants, their quotas (--minutes --messages --agents --concurrent-calls --memory-facts --knowledge-chunks --numbers, the whole set at once; a flag left out is no limit and 0 refuses everything), the vendor keys an org brings |
keys issue · list · revoke |
an org's API keys: printed once, listed by fingerprint, revoked by UPDATE. issue --env production|sandbox --scope … --subject … --name …: which world the key opens, what it may do there, whose it is |
routes list · add · rm · seed |
which agent answers a number, from the next call; --env says in which world |
Reading a call
| verb | what |
|---|---|
sessions list |
the calls, newest first |
sessions show <call> |
one call, entry by entry, every metric whole |
sessions tail <call> |
follow a call as it happens |
sessions recording <call> |
where its audio was written |
chat --agent <slug> [--url] [--caller] |
a text call from the terminal, one line per turn |
sessions reads Postgres, not HTTP: it is the operator's door, and works with the gateway down.
The tenant's own commands — pinecall run, chat, test, simulate, eval, ui — are the
agents repository's, and speak to this gateway with the org's key.
Where the rest is
ARCHITECTURE.md |
LiveKit's half and ours, the entities, the wire, the two processes, the path of a call, the log, the rings |
docs/scaling.md |
one server to a fleet: the three planes, seats not CPU, the heartbeats, cordon, the loop, overflow at the door |
docs/the-fleet.html |
the same fleet on one page for a reader: every way to run it, every verb and door, what was measured, what is left and why |
docs/a-box-in-production.md |
a box in production: one machine with a domain, from an operating system and nothing else — written by deleting a real box's software and putting it back |
docs/from-zero.md |
start here: a runtime of your own and an agent answering, every command run and every output what came back |
docs/the-runtime-cli.md |
every pinecall-runtime verb: what it takes, what it speaks to, and the two walkthroughs |
docs/multi-tenancy.md |
orgs, keys and tenants: what a key IS, why a laptop runs the same runtime a box does, and how a tenant is given one |
docs/protocol/gateway-api.md |
every door a tenant's own code may knock at, with an app in thirty lines |
docs/decisions/ |
why each module is the way it is, one page per module |
docs/protocol/ |
the operator API and the token door, as public contracts |
infra/box/README.md |
the box: standing one up, roles, slots, secrets, the fence, wiring a number |
infra/README.md |
the dev stack |
.env.example |
every variable both processes read, rendered from the settings class |
License
Apache-2.0. Use it, change it, run it in production, sell what you build with it — commercially or not, on your own box or somebody else's. The licence carries an explicit patent grant, which is why it is the one this stack uses (LiveKit's is the same). There is no NOTICE file, so nothing has to be reproduced downstream beyond the licence itself, and there is no CLA: a patch is yours and stays under the same terms.
pinecall on PyPI, and the box under infra/box/ — the same tree either way. Nothing in it
is a hosted-only path: everything needed to self-host is in this repository.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pinecall-0.1.0.tar.gz.
File metadata
- Download URL: pinecall-0.1.0.tar.gz
- Upload date:
- Size: 7.2 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5c6f0f7ec9afd0f9bbe2ba6fd28f331f3b13b0c0af79c4262a203a396a10b7f2
|
|
| MD5 |
2e6fd661f4a5e96991c061c2a023984b
|
|
| BLAKE2b-256 |
bc46d12c24fbaea653409277930f08a353da34e2eb7226c4dc753bf8a8b61fc6
|
File details
Details for the file pinecall-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pinecall-0.1.0-py3-none-any.whl
- Upload date:
- Size: 1.2 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
07cc59796505950e1fdd7aea988745e378983affd02f8f9977ba55d812ed788c
|
|
| MD5 |
1126909c59714dfd54aeead695e24c26
|
|
| BLAKE2b-256 |
60898d8f15f8a4fe121909022bfcc4d268d4d6d102de313977e406703daaf167
|