Quickstart · Docs · CLI · Self-hosting · Gateway API · Console
Pinecall runs voice and chat agents that you write as a class. The class's fields are the
call's state, its @tool methods are what the model may do, and its docstrings and render() are
the prompt. Everything else is Pinecall's job: the phone line, the browser call, WhatsApp, speech
in and out, turn-taking, memory, retrieval, judges at hang-up, the console and the bill.
This repository holds all of it: the runtime that runs the calls, the console it serves, the SDKs an agent is written with, and the CLI that drives them. Every package is released together, at one version, by one tag.
▸ The agent is a class
import { Agent, tool, type Stages } from "@pinecall/agents";
/**
* You answer the phone for the business. Short sentences: everything you say is read aloud.
* For anything the documents do not answer you take a message: who is calling and what about.
*/
export default class FrontDesk extends Agent {
stage: Stages<"ask" | "done"> = "ask"; // state: plain fields, kept per call
message?: { name: string; about: string } | undefined;
/** Writes down the caller's message, once they have said their name and what it is about. */
@tool({ stage: "ask", pii: ["name"] }) // a verb the model may call, in this stage
async takeMessage(name: string, about: string) {
this.message = { name, about };
this.stage = "done";
return this.message;
}
override render() { // the prompt's last block, a function of state
return this.message
? <p>The message is written down: {this.message.name}. Say goodbye and hang up.</p>
: <p>Greet the caller, and ask for their name and what the call is about.</p>;
}
}from typing import Literal
from pinecall import Agent, tool
class FrontDesk(Agent):
"""You answer the phone for the business. Short sentences: everything you say is read aloud."""
stage: Literal["ask", "done"] = "ask"
message: dict[str, str] | None = None
@tool(stage="ask", pii=("name",))
def take_message(self, name: str, about: str) -> dict[str, str]:
"""Writes down the caller's message, once they have said their name and what it is about."""
self.message = {"name": name, "about": about}
self.stage = "done"
return self.messagerequire "pinecall"
# You answer the phone for the business. Short sentences: everything you say is read aloud.
class FrontDesk < Pinecall::Agent
stage :ask, :done
state :message
# Writes down the caller's message, once they have said their name and what it is about.
tool stage: :ask, pii: %i[name]
def take_message(name:, about:)
self.message = { name:, about: }
self.stage = :done
message
end
endThe voice, the model, the greeting, the words it must pronounce and the documents it searches are
not in the class. They are settings of the agent, per environment and versioned, set with
pinecall agent set or in the console.
▸ From an empty folder to a phone call
The runtime is open source. Run it on your own machine, with no account, or use Pinecall Cloud. The agent is the same either way.
| on your machine | on Pinecall Cloud |
|---|---|
$ uv tool install "pinecall-runtime[voice]"
$ pinecall-runtime setupReads your machine and runs the stack that fits it — Enter to use it, or change a stage — and asks
your email and a password for the console. This machine is then signed in to it: no |
$ pinecall login
|
Then, in either case:
$ npm i -g pinecall
$ pinecall new acme --agent front-desk # --python or --ruby for the other SDKs
$ cd acme && npm install
$ pinecall chat # a written call against your class, here
$ pinecall test # the goldens, judged by a model
$ pinecall start # hold the agent: the console's calls reach your process
$ pinecall deploy up # run it on the runtime, without your laptop▸ How a call moves
- A caller comes in by a carrier, the browser widget, a written chat or WhatsApp.
-
A worker takes the call. It runs LiveKit's
AgentSession, with speech-to-text, the model and text-to-speech built from the org's providers. -
The gateway asks your class to act. When the model calls a tool, the gateway sends
tool.calldown your process's app socket, and your method's answer comes back astool.result. - Everything is an entry in the call's log. Turns, tool calls, latencies, costs and scores are appended, never rewritten. The console, the CLI, the bill and the judges all read that log.
The runtime is one Python package shaped like a Rails app. Each domain has its models, schemas, repository, service and endpoints, in a fixed bottom-up order that a test enforces. The architecture page walks every folder.
▸ Many orgs, one runtime
One runtime serves many businesses, and each one is an org: its own people and roles, keys, numbers and carriers, vendor keys, quotas, usage and data. Each org writes its own agents as classes, in its own repository, and each agent is a folder with its tests beside it.
- Your code never runs inside the runtime. The worker runs the speech pipeline; the class runs in the org's own process, or in a gVisor-sandboxed pod of its own when the runtime hosts it, and the gateway asks it to act over its app socket. One shared fleet of workers serves every org, and no two orgs' code share a process.
-
The database holds the line. Every request is scoped to its key's org, and Postgres
row-level security shows each connection that org's rows alone, a query that forgot its
WHEREincluded. - Two environments per org. The sandbox and production share the gateway and the database, never the workers: each has its own fleet. In the sandbox every person gets a copy of the agents of their own.
- Settings live per org, per environment, versioned. The voice, the model, the greeting, the lexicon, what the agent knows and remembers, its numbers and its routes. Quotas say what an org may use, and an org may bring its own vendor keys.
Pinecall stands on LiveKit: its media server, its SIP bridge and its Agents framework, whose
AgentSession runs every call. In that framework the agent's code runs inside the worker. Pinecall
adds the platform around it: the orgs, the people and their keys, the agent's code kept in the
tenant's process, the settings, the log, the judges, the console and the bill.
Orgs, keys and the two environments has
the whole model.
▸ Test it like software
| ring | what runs | command |
|---|---|---|
0 |
the class as plain code: tools, state, the prompt, no network | npm test |
1 |
goldens: a conversation the model must hold, judged turn by turn | pinecall test |
2 |
a persona calls the agent over a real line, scored at hang-up | pinecall simulate --voice |
3 |
one real call re-checked: latency, errors, consent, talk share | pinecall eval <call> |
4 |
every finished call, judged at hang-up: the verdict is an entry in its log | nothing to run |
A broken call becomes a case: kept, fixed, then approved or dismissed (pinecall cases).
Judges, monitors and alerts watch production (pinecall judges, pinecall monitors).
▸ What is inside
| folder | what it is | published as |
|---|---|---|
runtime/ |
the gateway that answers every door of the API, and the worker that runs the calls on LiveKit |
pinecall-runtime · PyPI |
cli/ |
the pinecall CLI and its MCP server, the same for every language |
pinecall · npm |
console/ |
the page the gateway serves, both environments in it | inside pinecall-runtime
|
widget/ |
one agent as one button on any page | inside pinecall-runtime
|
sdk/typescript/ |
the TypeScript framework an agent is written in |
@pinecall/agents · npm |
sdk/python/ |
the Python SDK |
pinecall · PyPI |
sdk/ruby/ |
the Ruby SDK |
pinecall · RubyGems |
packages/room/ |
the caller's side of a call, for a page's own UI |
@pinecall/room · npm |
packages/core/ |
what the console and Pinecall's supervisor app stand on | taken as source |
examples/ |
Clínica Norte, the one example agent, in each language | read and copied, never published |
infra/ |
the charts, the images and the Terraform a runtime is deployed with |
make deploy from runtime/
|
Each folder's README.md says what it is and how to use it.
| stage | verbs |
|---|---|
| start |
login · new · generate · whoami
|
| hold and talk |
start · chat · prompt · console · supervise
|
| the agent's settings |
agent · lexicon · voices · pipeline · providers
|
| testing |
test · simulate · eval · runs · cases · personas · judges · judging
|
| knowledge and memory |
docs · memory · remember
|
| the phone |
numbers · carriers · line · callbacks
|
| production |
deploy · secrets · sessions · monitors · webhook · telemetry · limits · data
|
| assistants |
mcp: the CLI as an MCP server, and mcp install for every assistant on the machine |
pinecall <verb> --help explains each one, and the
CLI reference has a page per verb.
▸ Working on it
$ git clone https://github.com/pinecall/pinecall && cd pinecall
$ pnpm install # every TypeScript package, one workspace and one lockfile (Node 24, pnpm 11)
$ cd runtime && uv sync # each Python package is a project of its own, with its own uv.lockThe TypeScript packages share the root's workspace (pnpm-workspace.yaml). runtime/ and
sdk/python/ both ship the module pinecall, so they are never one environment: each keeps its
own uv.lock. sdk/ruby/ has its Gemfile. Each package's own check is what CI runs for it
(.github/workflows/check.yml), only when its folder changed.
▸ Releasing
Every published package carries the same version. scripts/version prints it, or says which
package disagrees; scripts/version X.Y.Z writes it into all six. A v* tag publishes them all
(.github/workflows/release.yml), each registry by Trusted Publishing, and a GitHub Release whose
notes are each package's CHANGELOG.md section for that version.
▸ Security
Report a vulnerability as SECURITY.md says.
$ pinecall --help · docs.pinecall.io · pinecall.io