Skip to main content

pinecall

Build voice and chat agents as Python classes. Annotated fields are the agent's state, @tool methods are what the model can call, docstrings are what it reads about them, and a Jinja view renders the part of the prompt that changes during the call.

from typing import Literal

from pinecall import Agent, state, tool

from clinica import agenda  # your own system: a CRM, an API, a database


class ClinicaNorte(Agent):
    """You are the front desk of Clínica Norte. Formal, short sentences."""

    stage: Literal["identify", "book"] = "identify"
    patient: dict[str, str] | None = state(None, pii=True)
    slots: list[dict[str, str]] = []

    @tool(stage="identify", pii=("name", "phone"))
    def find_patient(self, name: str, phone: str) -> dict[str, str] | None:
        """Finds the patient by name and phone. Ask for both before calling it."""
        self.patient = agenda.find(name, phone)
        if self.patient:
            self.stage = "book"
        return self.patient

    @tool(stage="book", preview=2)
    def free_slots(self, day: str) -> list[dict[str, str]]:
        """Free slots on one day."""
        self.slots = agenda.free(day)
        return self.slots

views/clinica-norte.jinja, beside the class, renders the prompt from that state:

{% if stage == "identify" %}
Greet the caller and ask for their name and phone. Nothing else until the patient is identified.
{% endif %}
{% if remembers("usual doctor") %}
Offer their usual doctor's slots first.
{% endif %}
{% if slots %}
## Free slots, in order

{% for slot in slots %}
{{ slot.when }} with {{ slot.doctor }}
{% endfor %}
{% endif %}

The prompt has four named blocks: identity, knowledge and tools are static and cached before the history; view comes after it and is replaced every turn. A block is re-sent only when its text changes.

Memory and knowledge-base results never enter the prompt. The platform runs recall and search and returns their results to the model as tool results, so retrieved text is treated as data, not instructions. A view can ask remembers("usual doctor") and write its own sentence about the answer.

Five minutes

uv add pinecall, or pip install pinecall. PyPI pinecall 0.1.0 to 0.1.5 are the runtime's releases, before it moved to pinecall-runtime: a project pins pinecall>=0.1.6.

This package is a library: the verbs are the one pinecall CLI's, the same for every language. In a project laid out as agents/<name>/agent.py, with this package in its .venv:

npm i -g pinecall                     # the CLI: Node, whatever the agent is written in
pinecall login                        # this machine signed in, once; pinecall.json names the org
pinecall prompt --state agents/<name>/test/goldens/a.json   # the exact prompt a state produces. No gateway.
pinecall chat                         # the agent served from this terminal, and a caller against it
pinecall test                         # ring 1: the goldens, scored by the gateway
pinecall start                        # registered and answering: the process you deploy

The CLI never loads your class: for prompt, chat, test and start it runs this package's serve entry, .venv/bin/python -m pinecall.serve, and talks to it through the gateway. The console, the knowledge bases, memory, keys and every other verb are the CLI's. How a server runs the agent, with the CLI or inside your own Python process, is docs/production.md.

Architecture

This package is the application side. It never talks to a model vendor or handles audio: it sends commands and reads log entries over one WebSocket, in the runtime's own wire (pinecall.wire), held to the runtime's golden call log. Tools and hooks are the only code that may change state.

you want you use
the class and the view pinecall.Agent, pinecall.bridge.mount
the agent held inside your own process await pinecall.hold(ClinicaNorte, url=…, api_key=…)
the socket alone, your own way of deciding what to answer pinecall.Client
a suite with no network, no key and no model pinecall.testing
import asyncio
import os

from pinecall import Client
from pinecall.wire.events import UserTurnEnded


async def main() -> None:
    pc = Client("https://cloud.pinecall.io", os.environ["PINECALL_KEY"])
    agent = pc.agent("clinica-norte")

    def heard(event, call):  # every turn.user of every call this socket serves
        if isinstance(event, UserTurnEnded):
            call.say(f"I heard: {event.text}")

    agent.on("turn.user", heard)
    await pc.connect()
    await asyncio.Event().wait()


asyncio.run(main())

PINECALL_KEY here is a server's token, made in the console's API keys screen for the org and the environment the process runs in — not your own sign-in, which stays on your machine. On a runtime of your own the URL is its gateway's (http://127.0.0.1:8080 on your machine).

Testing it

Agent tests run with no network, key, model or gateway, in a plain pytest:

from pinecall.testing import Gateway, load

ClinicaNorte = load("agents/clinica-norte/agent.py")

with Gateway() as pc:
    pc.mount(ClinicaNorte)
    call = pc.call_started(from_="+34600123456")
    call.tool("find_patient", name="Marta Ruiz", phone="600123456")

    assert "ask for their name" not in call.prompt
    assert call.tools == ["free_slots"]

Where the rest is

what where
tutorial: an agent with knowledge and memory, from an empty directory docs/tutorial.md
every file, entity and rule ARCHITECTURE.md
how to write an agent, step by step docs/writing-an-agent.md
the view, and the blocks of a prompt docs/the-view.md
how to test one docs/testing-an-agent.md
running it in production docs/production.md
the CLI, verb by verb the one CLI's reference, at docs.pinecall.io
a complete example agent examples/python/clinica_norte
the wire itself src/pinecall/wire/, the runtime's shapes this package speaks

Developing

uv sync
make check        # ruff, pyright strict, deptry, the suite and the example's own

Requirements

Python 3.11+. Runtime dependencies: pydantic, Jinja2, websockets and httpx. The pinecall CLI is a Node program (Node 24+): the verbs need it, the library does not.

License

Apache-2.0.

Metadata

Release files for pinecall 0.11.10

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pinecall 0.11.10
File Size Uploaded Provenance
pinecall-0.11.10.tar.gz 87.5 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for pinecall 0.11.10
File Interpreter ABI Platform
pinecall-0.11.10-py3-none-any.whl Python 3 none any Details

Total release size: 193.7 kB

Release files / pinecall-0.11.10.tar.gz

Download URL pinecall-0.11.10.tar.gz
Size 87.5 kB
Tags Source
SHA-256 checksum
How to use checksums
f1601667caad21246897687c43bc2a9cb97cc3c8e7300ed7a5a9d28648bb40be
BLAKE2b-256 checksum
How to use checksums
e3b4e5d838bd427966c771e8fbf12822d77eb10cc0dfa7c0008e6bc72921ee15
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

Release files / pinecall-0.11.10-py3-none-any.whl

Download URL pinecall-0.11.10-py3-none-any.whl
Size 106.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d593db032a22d77c96c2a8c99296d2414ba3710d6dc7899c4d340f0208d3fbb7
BLAKE2b-256 checksum
How to use checksums
37dab319179151c8834c283eef59c3bd4b667499f21c7afad64857009f236597
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

Security for pinecall 0.11.10

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for pinecall-0.11.10.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page