Metadata-Version: 2.4
Name: backplanes
Version: 0.1.0
Summary: Backplanes Python SDK for agent telemetry and identity management.
License-Expression: LicenseRef-Proprietary
Project-URL: Homepage, https://backplanes.com
Project-URL: Repository, https://github.com/Backplanes/python-sdk
Project-URL: Issues, https://github.com/Backplanes/python-sdk/issues
Keywords: backplanes,telemetry,agents,observability
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: grpcio>=1.76.0
Requires-Dist: protobuf>=7.35.1
Requires-Dist: cryptography>=41.0.0
Provides-Extra: dev
Requires-Dist: fastapi>=0.115.0; extra == "dev"
Requires-Dist: httpx>=0.27.0; extra == "dev"
Requires-Dist: pyright>=1.1.411; extra == "dev"
Requires-Dist: pytest>=8.0.0; extra == "dev"
Requires-Dist: requests>=2.32.0; extra == "dev"
Requires-Dist: ruff>=0.9.0; extra == "dev"
Dynamic: license-file

# Backplanes Python SDK

The Backplanes Python SDK lets Python agents register with a Backplanes collector, capture LLM and HTTP telemetry automatically, propagate workflow context between agents, and persist agent identity locally — with one call to set up.

## Table Of Contents

- [Install](#install)
- [Quick Start](#quick-start)
- [How Instrumentation Works](#how-instrumentation-works)
- [Modes](#modes)
- [Configuration And Identity](#configuration-and-identity)
- [Running Alongside Your Application](#running-alongside-your-application)
- [Custom Events](#custom-events)
- [What The SDK Covers](#what-the-sdk-covers)
- [Examples](#examples)
- [Docs](#docs)
- [Development](#development)

## Install

```bash
pip install backplanes
```

For local development:

```bash
uv pip install -e .
```

## Quick Start

Get a key file from the dashboard: **Settings → API keys → Create key → Download key file**, and save it as `~/.backplanes/backplanes-key.json`. (Or set `BACKPLANES_ORG_API_KEY` — the key file additionally carries a signing key, which you need to hand workflows to other agents.)

```bash
export BACKPLANES_ORG_API_KEY="OK_..."   # alternative to the key file
```

Then instrument your agent with one call:

```python
import anthropic
import backplanes

backplanes.init(agent_name="my-agent")

sdk = anthropic.Anthropic()
response = sdk.messages.create(
    model="claude-haiku-4-5-20251001",
    max_tokens=50,
    messages=[{"role": "user", "content": "Say hello in five words."}],
)
```

That's the whole integration. Every Anthropic or OpenAI client constructed after `init()` is instrumented automatically — the call above lands in the dashboard as an `llm` event with provider, resolved model, and prompt/completion token counts, including for streaming calls. This script contains zero hand-written telemetry.

`init()` returns a `BackplanesClient` for everything beyond automatic capture — starting workflows, custom events, context propagation:

```python
client = backplanes.init(agent_name="my-agent")
workflow_id = client.start_workflow()
```

## How Instrumentation Works

Instrumentation is three composable layers, highest fidelity first. See [docs/design/INSTRUMENTATION_TIERS.md](docs/design/INSTRUMENTATION_TIERS.md) for the full design.

**SDK wrappers** (`wrap_anthropic`, `wrap_openai`) wrap an SDK client instance's own `create`/`stream` methods, recording each call at the semantic level: resolved model, prompt and completion tokens, and cache tokens. Streaming responses are observed chunk by chunk, so token counts land even for streamed calls. Use these directly when you want explicit per-client control:

```python
import anthropic
import backplanes

client = backplanes.init(agent_name="my-agent", auto_instrument=False)
sdk = backplanes.wrap_anthropic(anthropic.Anthropic(), client)
```

**`init()` constructor hooks** give you wrapper fidelity without touching client construction: `init()` hooks the Anthropic/OpenAI SDK constructors, so every client built afterwards comes out wrapped. Clients constructed *before* `init()` fall through to the transport net.

**The transport net** (`patch_httpx`, `patch_requests`) instruments outgoing HTTP at the `httpx` (sync and async) and `requests` level. It catches everything the wrappers don't — unknown SDKs, raw HTTP, pre-`init()` clients — recognizing known inference endpoints and emitting `llm` events with provider, model, and token counts where the response allows. It also carries `Backplanes-Context` propagation to every destination. Streamed responses caught only at this level record the call but not token usage, and are marked as such in event metadata.

The layers compose because of one dedup guarantee: **one logical call produces one event**. Wrappers mark the duration of the underlying SDK call, and the transport net demotes itself inside that window — context propagation still runs, event emission does not. An SDK's internal retries land inside the same window, so they never double-count.

## Modes

### Workflow Root

Use workflow root mode when your agent starts workflows. This needs an org API
key, passed directly or read from `BACKPLANES_ORG_API_KEY`.

```python
client = backplanes.init(
    org_api_key="OK_...",
    agent_name="my-agent",
)
```

A key file bundles the same org API key with an Ed25519 signing key, and the
client reads both from it:

```python
client = backplanes.init(
    config_file="backplanes-key.json",
    agent_name="my-agent",
)
```

Without an explicit `config_file`, the client searches `BACKPLANES_KEY_FILE`, `./backplanes-key.json`, and `~/.backplanes/backplanes-key.json` — see [docs/CONFIGURATION.md](docs/CONFIGURATION.md).

### Propagating To Other Agents

Handing a workflow to a second agent means signing a `Backplanes-Context`, and
that needs the signing key from a key file. An org API key on its own won't do
it. `can_propagate_context` tells you which you have. Without a signing key each
agent's events still carry their own workflow ID, they just aren't linked into
one chain.

The `Backplanes-Context` header identifies your org, workflow, and agent chain,
and the transport patches send it to every destination:

```python
backplanes.init(agent_name="my-agent")
```

That is what lets a chain link without anyone describing the topology first, and
it has a disclosure consequence worth being explicit about: the same process
usually calls both your own services and third-party APIs, and those third
parties receive the header too.

### Mid-Chain Agent

Use mid-chain mode when your agent receives a `Backplanes-Context` header from an upstream agent:

```python
client = backplanes.init(agent_name="worker-agent")

context_jwt = request.headers.get("Backplanes-Context")
with client.request_context(context_jwt):
    ...  # work in this scope is attributed to the upstream workflow
```

### Local Development

By default the client targets `api.backplanes.com:443` with TLS enabled. To point the same code at a different collector, use the environment:

```bash
export BACKPLANES_COLLECTOR_HOST=localhost
export BACKPLANES_COLLECTOR_PORT=50051
```

For a collector behind a private certificate authority, keep TLS on and point
`BACKPLANES_TLS_CA_FILE` at the CA bundle — gRPC does not read the OS trust
store. The same settings are available as constructor parameters
(`collector_host`, `collector_port`, `tls_ca_cert`); precedence is
explicit parameter > key file > environment > default.

### Claiming And Unclaimed Mode

A first run without org credentials registers the agent and logs a claim URL.
Claim it in the dashboard to link the agent to your organization; the client
picks the claim up on its next call and starts sending. With a key file or
`BACKPLANES_ORG_API_KEY` in place, the agent is active immediately and no
claiming step is needed.

Until an agent is claimed:

- `start_workflow()` returns a placeholder workflow ID
- `get_context_header()` returns `{}` because there is no active signed workflow context
- `ingest()` returns a stub response instead of sending events

An agent leaves unclaimed mode two ways:

- A key file appears in one of the standard search paths. The client hot-reloads it on the next call and gains org credentials *and* signing keys, so it can also mint `Backplanes-Context` JWTs for downstream agents.
- The collector reports the agent as claimed. `refresh_claim_status()` asks it directly, and the client polls at most once a minute during normal calls. This is what happens when you claim an agent from the dashboard, where there's no key file to download. The client can ingest and stamps its org onto events. Signing context JWTs still needs a key file.

## Configuration And Identity

Every collector setting resolves from constructor parameters, the key file, and
the environment, in that order. Key files only override the collector settings
they actually specify. Named agents get their own identity file
(`~/.backplanes/identities/<agent_name>.json`), so several agents on one
machine stay distinct identities. The full environment variable table, key file
schema, and identity file locations are in
[docs/CONFIGURATION.md](docs/CONFIGURATION.md).

## Running Alongside Your Application

If the collector has problems, your application should keep working.

- **Construction does not raise by default.** When the collector is unreachable the client
  logs a warning and starts up with telemetry off, retrying registration in the
  background. Pass `strict=True` if you would rather it raise
  `BackplanesNotReadyError` — useful in CI, where silent telemetry is worse
  than a failed build.
- **`is_ready` answers "would an event reach the collector".** It is False while
  the agent is waiting to be claimed, which is where every new agent starts.
  `has_identity` is the narrower question of whether registration succeeded.
  An unclaimed client warns, so this state is visible without checking.
- **Every call has a timeout**, 10 seconds by default and configurable. A host
  that drops packets silently will fail the call instead of hanging your
  thread.
- **Ingest never blocks you.** `ingest()` starts the call and hands back the
  future for it. gRPC does the network work on its own threads. Ask for the
  answer only when you want it:

  ```python
  client.ingest(event)                  # fire and forget
  client.ingest(event).result()         # wait for the collector's counts
  ```

  At interpreter exit the client waits up to three seconds for whatever is
  still in flight, so a short script usually doesn't need to do anything. That
  wait is best effort and gives up quietly — if you need to know an event was
  accepted, call `result()`. `flush(timeout=...)` waits for everything
  outstanding and returns False if it ran out of time, which is what you want
  before returning from a serverless handler. A call nobody waits on logs its
  failure, since there's nobody to raise to.

## Custom Events

Automatic instrumentation covers LLM and HTTP traffic. For everything else —
tool invocations, internal pipeline stages, domain-specific events —
`create_event(...)` and `ingest(...)` are still there:

```python
client = backplanes.init(agent_name="my-agent")

event = client.create_event(
    edge_type="tool",
    direction="egress",
    status="ok",
    metadata={"tool.name": "send_email"},
)
client.ingest(event)
```

If your agent is handling an incoming Backplanes workflow, wrap the work in
`request_context(...)` so events are attributed to the upstream chain:

```python
context_jwt = request.headers["Backplanes-Context"]
with client.request_context(context_jwt):
    client.ingest(client.create_event(edge_type="tool", direction="ingress"))
```

`create_event(...)` supports the full event schema — sessions, instance IDs,
parent events, PII fields, token usage, error info, custom metadata. See
[docs/ADVANCED.md](docs/ADVANCED.md), and
[`examples/email_campaign`](examples/email_campaign) for tool and PII events in
a real workflow.

## What The SDK Covers

- `init(...)` — one-call setup: constructs the client, hooks the Anthropic/OpenAI SDK constructors, installs the transport net
- `wrap_anthropic(...)` / `wrap_openai(...)` — semantic wrappers for explicit per-client instrumentation, with streaming token capture and cache token metadata; the OpenAI wrapper covers `chat.completions`, `responses`, and `embeddings`, and attributes OpenAI-compatible endpoints (gateways, Groq, and the like) by `base_url`
- `patch_httpx(...)` — the transport net for anything built on `httpx`, sync *and* async clients. Recognized inference endpoints emit an `llm` edge with provider, resolved model, and token counts pulled from the response
- `patch_requests(...)` — the same net for outgoing `requests` calls
- `instrument_fastapi(...)`, `instrument_asgi(...)`, and `instrument_wsgi(...)` — one-line framework setup for inbound context and ingress telemetry (Django is WSGI/ASGI)
- `request_context(...)` — request-scoped propagation in web apps and workers
- `create_event(...)` — the full event schema, including `session_id`, instance IDs, `parent_event_id`, PII fields, `schema_version`, token usage, error info, and custom metadata
- `create_batch(...)`, `ingest_batch(...)`, and `ingest_stream(...)` — explicit batch construction, unary batch ingest, and the collector's client-streaming RPC

Both HTTP patches record calls to hosts you don't own against an ID derived
from the hostname, so you can see which third party received the data. That is
unconditional — it needs no argument, and sends nothing to the third party.

The `Backplanes-Context` header is also unconditional: every destination gets
the org, workflow, and agent chain while a workflow context is active. That
includes third-party APIs, so enable the transport patches only where that
disclosure is acceptable:

```python
patch_httpx(client)
patch_requests(client)
```

You can also import the generated proto modules directly:

```python
from backplanes.v1 import collector_pb2, events_pb2
```

## Examples

Runnable, real-world examples — from a five-line quickstart to a multi-agent
workflow with context propagation — live in [`examples/`](examples), each with
its own README.

## Docs

Focused docs live in [`docs/`](docs):

- [Configuration And Identity](docs/CONFIGURATION.md)
- [Advanced Usage](docs/ADVANCED.md)
- [Integrations](docs/INTEGRATIONS.md)
- [Framework And App Placement](docs/FRAMEWORKS.md)
- [Context Model](docs/CONTEXT_MODEL.md)
- [FastAPI And ASGI Apps](docs/FASTAPI.md)
- [Django And WSGI/ASGI Apps](docs/DJANGO.md)
- [A2A / A2P / MCP / Worker Patterns](docs/A2A_A2P.md)
- [Releasing](docs/RELEASING.md)

## Development

```bash
uv sync
uv build
uv run ruff check .
uv run pytest
```

### Regenerating Protobuf Bindings

`backplanes/v1/` is generated from the Backplanes `proto/` buf module and
checked in, so installing this package needs neither `buf` nor `protoc`. To pick
up proto changes, point `BACKPLANES_PROTO_REPO` at a checkout holding that
module:

```bash
BACKPLANES_PROTO_REPO=/path/to/checkout ./scripts/gen-proto.sh
```

Plugin versions are pinned in [`buf.gen.yaml`](buf.gen.yaml) so the output is
reproducible. The `protocolbuffers/python` plugin decides the gencode version
stamped into every `_pb2.py`, and the protobuf runtime won't load gencode newer
than itself. So if you bump that pin, bump the `protobuf` floor in
`pyproject.toml` too. The script fails when the two disagree, which is better
than an import error in someone else's app.
