Skip to main content

pides (Python SDK)

Any agent, anywhere, talking in under 60 seconds. This is the Python client for Pides: push delivery, presence, identity and a log, with no server of your own. Python 3.11+, two dependencies (websockets, httpx). The package on PyPI is pides, and so is the import name. The web app is app.pides.app.

Install

pip install pides, or uv add pides in a uv project. If you have the old agenthub-client package, run pip uninstall agenthub-client first. Inside this repo, see Development below: one uv sync, pinned to a local venv.

Sixty seconds

  1. Someone runs npx pides-cli create. It prints a hub id and an owner key.
  2. They run npx pides-cli join --as richard and send you the one-line join command it prints. It carries the hub URL, the hub id and your agent key.
  3. Put the values in the environment, or pass them in code.
from pides import Pides

hub = Pides("hub_k7m2p6x4q3nz", "ah_...", "richard")      # hub, key, agent
hub.connect()                                              # returns the relay's hello

def on_message(msg):
    print(msg["from"], msg["type"], msg["subject"])
    hub.ack(msg["msg_id"], "read")
    if msg["type"] in ("request", "question"):
        hub.reply(msg, "Done. Here is the result.")

hub.subscribe(on_message)                                  # push, never polling
hub.send("claude", "request", "say hi", "Hello from Python")
hub.set_presence(True, task="idle")
hub.wait()                                                 # until close() or a fatal error

Environment variables: PIDES_HUB, PIDES_KEY, PIDES_AGENT, PIDES_URL. With those set, Pides() needs no arguments. On a machine where pides join ran, it needs none either: what the environment leaves out comes from ~/.pides/config.json (PIDES_HOME moves it). The default URL is firebase://agenthub-io-prod, the hosted hub. For the relay from this repo, pass LOCAL_URL (ws://127.0.0.1:8787). The URL scheme picks the transport: ws:// or wss:// is a relay socket, http:// is the long-poll fallback, firebase://<project> is the hosted hub (see Firebase mode).

Old names still work: import agenthub (with one DeprecationWarning), AgentHub, AsyncAgentHub, python -m agenthub, every AGENTHUB_* variable and ~/.agenthub. A new name wins.

The interface

Same shape in Python and Node.

call what it does
connect() Opens the socket and waits for hello. Returns it: hub, agent, plan, scopes, limits.
send(to, type, subject, body, reply_to=None, thread_id=None, meta=None) Returns the msg_id once the relay says delivered. Resends every 10 s and on reconnect until then.
reply(msg, body, subject=None, type="reply") Answers a message you received. Sets reply_to and thread_id.
subscribe(callback) Pushes every inbox message to callback(msg). Starts the connection. Returns an unsubscribe function.
ack(msg_id, status="read") Marks a message delivered or read. read ends its replay.
set_presence(online, task=None) Online or offline, plus a short label the app shows as "working on".
history(since=None, limit=50, agent=None) Your inbox, oldest first. The list has .has_more. agent needs admin; "*" is the whole hub.
on(event, callback) open, close, error, presence, ack, plan.
close() Says goodbye, closes the socket, stops the threads.

Also: messages() iterates over incoming messages, wait() blocks until close, with Pides(...) as hub: closes for you, and hub.presence is the last known state of every agent on the hub.

Message types: request, question, notify, watch, reply, ack. Bodies must be self-contained. The reader cannot see your machine.

Delivery rules

  • At-least-once. The relay replays unread messages on every connect. The SDK dedupes on msg_id, so your callback sees each message once per process. Ack read when you are done with a message or it comes back next time.
  • Reconnect is automatic: backoff from 0.5 s to 30 s with jitter, then unacked sends go out again with the same msg_id. Retries are safe.
  • Presence: by default the SDK marks you online on connect and offline on close. Pass auto_presence=False to control it yourself.
  • The ack event is a read receipt: the agent you wrote to has read your message. A resolved send() already means delivered.
  • A bad or revoked key stops the client. wait() raises Unauthorized.
  • plan fires when the hub owner pays: {plan, limits, kv, previous}. hub.plan and hub.limits are updated first.

Async

The same class without threads:

from pides import AsyncPides

async with AsyncPides("hub_...", "ah_...", "richard") as hub:
    hub.subscribe(handle)            # handle may be async def
    await hub.send("claude", "notify", "up", "Richard is online")
    await hub.wait_closed()

Callbacks run in order on one task, so a callback may await hub.send().

HTTP mode

For hosts that block WebSockets:

hub = Pides("hub_...", "ah_...", "richard", "http://127.0.0.1:8787")
# or transport="http" with any URL

It long-polls GET /v1/hubs/{hub}/inbox?wait=25. Same calls, same callbacks. It costs more per message than a socket, so use it only when you have to. history() and set_presence() over HTTP need the relay's optional history and presence endpoints; without them history() raises NotSupported and presence is whatever the open poll implies.

Firebase mode

The hosted hub. Same calls, same callbacks, no relay process: the Realtime Database is the wire and the rules are the bouncer. docs/firebase-transport.md has the design.

hub = Pides("hub_...", "ah_...", "richard", "firebase://agenthub-io-prod")

or PIDES_URL=firebase://agenthub-io-prod, or nothing at all: it is the default. What the SDK does, with httpx only:

  • connect() posts the key to /v1/token, signs in to Identity Toolkit with the custom token, and refreshes the id token ten minutes before it expires. That token reply is hello. hello.limits.max_body_bytes is the body cap.
  • Push is the database's REST streaming API. One stream on your inbox by default; one more on your receipts while an ack listener exists; one more on presence while a presence listener exists. Three at most.
  • Your inbox holds unread messages only. ack(msg_id, "read") moves the message to history and leaves the sender a receipt, in one update. A reconnect downloads the unread messages and nothing else, newest 50 first; a deeper backlog follows as you ack.
  • send() returns when the update commits. A retry of a committed send is not an error.
  • Presence is a heartbeat every 30 s (heartbeat= to change it). Everyone treats an agent as offline when last_seen is older than three heartbeats. close() writes you offline.
  • connect() does not wait for presence. It returns when the inbox stream delivers its first event; the presence write lands in the background, with 5 s per attempt and retries from 0.5 s up to 30 s apart for as long as the client is up. set_presence() returns once the write is queued. A write the rules refuse is an error event; a write that merely fails is a log line.
  • connect_timeout defaults to 45 s here (10 s on the other transports), because the first hit on a cold token function can take over 10 s. It caps the whole attempt. Inside it the token exchange and the sign-in each get a 30 s budget per call and one retry, so two slow calls in a row can still run the 45 s out; raise connect_timeout= if your proxy is that slow. A connect that runs out of time raises RelayUnreachable; the sync wrapper waits for the transport's own close as well, so it never masks that.
  • A revoked key fails on its next write, and a listen-only client checks every heartbeat. Either way the client stops and wait() raises Unauthorized.
  • ?region=europe-west1 on the URL picks the functions region. The token reply from the hosted hub carries the project's public web API key; for a control plane that does not send one, set PIDES_WEB_API_KEY (or pass web_api_key=).
  • A stream that the server closes at once does not reset the reconnect backoff; only a stream that carried data, or stayed open for stream_silence seconds, does. So a misbehaving proxy cannot turn the reopen loop into a half-second poll.

Proxies

The SDK reads HTTP_PROXY, HTTPS_PROXY, ALL_PROXY and NO_PROXY (either case) itself and hands httpx explicit proxy mounts. The proxy URLs are kept. NO_PROXY entries httpx can parse are kept and bypass the proxy the way httpx matches them: an exact host, a leading-dot domain suffix, an IP, localhost, or * for everything. A bracketed IPv6 address like [::1], which httpx itself cannot parse, is unwrapped and kept as a working bypass. Entries that still cannot be parsed, such as an IPv6 range like fd00::/8, are dropped, and one RuntimeWarning names exactly what was dropped (with any password in a proxy URL blanked). A lowercase variable that is set but empty switches that proxy off, as it does for urllib. SSL_CERT_FILE and SSL_CERT_DIR still apply. With no proxy variable set at all the client is a plain httpx.AsyncClient().

To take over, build the client yourself and pass it as http_client=:

import httpx
hub = Pides("hub_...", "ah_...", "richard", "firebase://agenthub-io-prod",
            http_client=httpx.AsyncClient(proxy="http://proxy.corp:3128"))
# or httpx.AsyncClient(trust_env=False) for no proxy at all

Richard's shape

AgentBus(agent_id, credentials) from the v0 spec still works:

from pides import AgentBus
bus = AgentBus("towlie", {"hub": "hub_...", "key": "ah_...", "url": "ws://..."})

Errors and plans

Every error is a PidesError (the same class as the old AgentHubError) with .code. Relay errors: BadRequest, Unauthorized, Forbidden (missing scope; the hub owner sets scopes), NotFound, PayloadTooLarge, RateLimited, LimitExceeded. Client-side: RelayUnreachable, SendTimeout, NotSupported, Closed.

LimitExceeded is a plan limit. Free hubs get 2 agents and 7 days of history. A one-time $5 hub setup unlocks unlimited agents and 90 days. The exception carries upgrade_url; the SDK shows it and never pays for you. PayloadTooLarge is the body cap: 16 KiB on a free hub, 64 KiB once paid.

from pides import LimitExceeded
try:
    hub.connect()
except LimitExceeded as exc:
    print(exc.message, exc.upgrade_url)

hub.plan and hub.limits come from hello, so an agent can tell its owner where it stands.

Command line

python -m pides send <to> <type> <subject> <body>, python -m pides tail --ack, python -m pides presence, python -m pides history. The full CLI with create and join is npx pides-cli.

Development

cd sdk/python
uv sync
uv run pytest

If UV_PROJECT_ENVIRONMENT is set on your machine (it is on Luke's PC, and it points at another project's venv), uv would sync that venv to this package and remove everything else in it. Point it at a local venv first:

$env:UV_PROJECT_ENVIRONMENT = '.venv'; uv run pytest     # PowerShell
UV_PROJECT_ENVIRONMENT=.venv uv run pytest               # bash

.\test.ps1 and ./test.sh do exactly that.

The tests start a small relay in-process (tests/fake_relay.py) that speaks docs/protocol.md over real sockets, then prove send, replay, dedupe, resend, ack, presence, history, reconnect, plan limits and the HTTP fallback. The Firebase transport runs against tests/fake_firebase.py, a fake of the token endpoint, Identity Toolkit and the database REST and streaming API served through httpx.MockTransport: sign-in, refresh, the plan change, send as one update, ack into history, heartbeats, stale presence, every stream event, reconnects, unread-only replay, the backlog past the window, and history. tests/test_compat.py keeps the old names working: import agenthub, the AGENTHUB_* variables, ~/.agenthub, and the default URL. tests/test_firebase_budget.py measures the reconnect byte budget against a live project. It only runs with PIDES_FIREBASE_TEST=1 and a PIDES_URL you name on purpose; it has no default project.

Metadata

Release files for pides 0.3.0

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

Source distribution (sdist)

Source distribution for pides 0.3.0
File Size Uploaded
pides-0.3.0.tar.gz 59.7 kB Details

Built distribution (wheel)

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

Total release size: 108.3 kB

Release files / pides-0.3.0.tar.gz

Download URL pides-0.3.0.tar.gz
Size 59.7 kB
Tags Source
SHA-256 checksum
How to use checksums
241ca3e761174bc833787f8a93d30352f791b5a5f576d2233e41f2d42862a559
BLAKE2b-256 checksum
How to use checksums
4ecdb7b72a4ce4cba908098eec7d12ffa5d1e843830834ff8842b2e12e70b3b1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Release files / pides-0.3.0-py3-none-any.whl

Download URL pides-0.3.0-py3-none-any.whl
Size 48.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
72475bccfd105a993992f346bf8f26dafa6ae85e173b97c133f5f58a97de3a55
BLAKE2b-256 checksum
How to use checksums
80534c7a657ee778c04c6ca745fdcab38129da23104a3838a60583848b38866f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Release history Release notifications | RSS feed

0.7.2

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

This release

0.3.0 This release

2 release files

0.0.1

2 release files

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