Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ jobs:
ruff check python tests examples
ruff format --check python tests examples

- name: Public documentation language
if: matrix.os == 'ubuntu-latest' && matrix.python == '3.11'
working-directory: sdk-python
shell: bash
run: |
if rg -n -i '\b(boundary|path|surface|authority|projection|lowering|flow|layer|contracts?)\b' \
README.md RELEASE_NOTES.md docs examples \
--glob '*.md' --glob '*.mdx'; then
echo "Public documentation must name the API, queue, process, service, or request directly."
exit 1
fi

- name: Python type contract
if: matrix.os == 'ubuntu-latest' && matrix.python == '3.11'
working-directory: sdk-python
Expand Down
38 changes: 23 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ recorded. Both behaviors are off by default.
## Handle permissions and source changes

PocketStation does not prompt during import or discovery. Check microphone
permission without prompting, then let Source opening report the authoritative
result. Application capture and microphone capture use separate permissions.
permission without prompting through
`pocketstation.sources.microphone_permission_observation()`, then let Source
opening report the authoritative result. Application capture and microphone
capture use separate permissions.

When an application or device disappears, PocketStation reports the change and
does not switch to another Source. Stop the current Session, discover again,
Expand All @@ -55,7 +57,7 @@ output as independent recorded stems. Provider events and media events share
one monotonic timeline, so you can see whether delay occurred before the model,
inside the provider, in local output, or after Relay delivery.

See the [voice-agent debugger instructions](examples/README.md#debug-a-voice-agent-interruption-from-the-media-boundary).
See the [voice-agent debugger instructions](examples/README.md#debug-a-voice-agent-interruption).
Run repository examples from a source checkout or source archive. The installed
`pocketstation-demo` command is the packaged application-and-microphone demo.

Expand All @@ -78,7 +80,7 @@ faster-whisper Operator, connects both stems to its audio input, and prints each
transcript with its original source identity. It does not start Relay or write a
recording.

The Session runs this path concurrently:
The Session runs these jobs concurrently:

```text
voice application ─┐
Expand Down Expand Up @@ -152,14 +154,14 @@ cancelled, and invalid-buffer outcomes explicitly.

## Create an integration

PocketStation uses four open boundaries:
PocketStation provides four integration APIs:

| Boundary | Use it when |
| API | Use it when |
|---|---|
| `Source` | Media or signals enter the Session. |
| `Operator` | Work transforms media or emits typed signals. |
| `Connector` | Media or signals leave for an external system. |
| `Endpoint` | You need the lower-level outbound execution contract. |
| `Endpoint` | You need direct control of an outbound worker. |

Pass one function when the destination is already open:

Expand All @@ -174,7 +176,7 @@ destination = pks_aio.Connector(send=send_audio)
application.send_to(destination)
```

Subclass the synchronous or asyncio contract when the provider opens and
Subclass the synchronous or asyncio Connector when the provider opens and
closes resources. The provider class owns its connection; the Session owns
bounded delivery, lineage, observations, drain, abort, and joined shutdown:

Expand Down Expand Up @@ -207,8 +209,8 @@ destination. See [Create an integration](docs/guides/integrations.md) for
deadlines, failures, and the advanced SPI.

Python provider callbacks execute on bounded off-realtime workers. They cannot
be used as native capture callbacks. Compiled native extensions remain the path
for native provider code, and process sidecars remain available when crash
be used as native capture callbacks. Use compiled native extensions for native
provider code, or a process sidecar when crash
isolation is required.

## Use Relay from Python
Expand All @@ -227,7 +229,7 @@ redacted secrets, and matching synchronous and asyncio APIs.
asyncio namespace provides awaitable lifecycle, stream, Relay, provider, and
audio-input operations without creating another audio queue.

Python callbacks still cross the interpreter boundary. Capture, routing,
Python callbacks still enter the interpreter. Capture, routing,
recording, and Relay transport remain native-speed; arbitrary Python model code
does not have the same execution cost as Rust.

Expand All @@ -236,7 +238,7 @@ does not have the same execution cost as Rust.
| Area | Support |
|---|---|
| Python | 3.11 and newer |
| macOS Apple silicon | Installed wheel, application capture, physical microphone, 10 ms voice path, Relay, Chromium, and multistem recording tested |
| macOS Apple silicon | Installed wheel, application capture, physical microphone, 10 ms voice capture, Relay, Chromium, and multistem recording tested |
| Linux | Core application selection and 10 ms capture tested; installed Python distribution qualification in progress |
| Windows 11 ARM64 | Core application selection and 10 ms capture tested in a VM; installed Python distribution and physical-device qualification in progress |
| WAN and TURN | Not yet qualified |
Expand All @@ -246,7 +248,7 @@ Connector `0.1.2`.

The Rust-to-Python audio read currently copies native samples into Python-owned
bytes before exposing a `memoryview`. The view avoids another Python-side copy;
the complete boundary is not zero-copy.
the call into Python still copies samples and is not zero-copy.

## Develop the SDK

Expand All @@ -266,16 +268,22 @@ uv run mypy python tests/qualification/typing_contract.py examples
API ownership.
- [Write application-owned audio](docs/guides/application-audio.md) — bounded
PCM input and selective output cancellation.
- [Process audio and typed signals](docs/guides/process-audio-and-signals.md) —
Operators, named ports, generated audio, and finite model work.
- [Record and observe a Session](docs/guides/record-and-observe.md) — multistem
outcomes, route metrics, and lifecycle events.
- [Keep each source identifiable](docs/concepts/source-identity-and-time.md) —
selection, persistence, timestamps, generations, and discontinuities.
- [Read events, metrics, outcomes, and errors](docs/reference/events-and-errors.md)
— setup failure, live observations, and terminal results.
- [Prepare each platform](docs/operations/platform-support.md) — permissions,
source persistence, explicit rediscovery, and fallback policy.
- [`examples/README.md`](examples/README.md) — runnable examples and
prerequisites.
- `pocketstation.capture` — concise application and microphone capture.
- `pocketstation.session` — Session declarations and lifecycle.
- `pocketstation.graph` — stems, ports, routes, and signal contracts.
- `pocketstation.graph` — stems, ports, routes, and signal specifications.
- `pocketstation.connector` — outbound provider authoring.
- `pocketstation.operator_authoring` — computation authoring.
- `pocketstation.source_authoring` — inbound provider authoring.
- `pocketstation.aio` — asyncio projection of the same engine.
- `pocketstation.aio` — asyncio APIs for the same engine.
20 changes: 17 additions & 3 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# PocketStation for Python release notes

## Unreleased

### Added

Advanced integrations can now configure `RouteSettings` as accepted media plus
a separate `DeliveryPolicy`. Connector, Endpoint, and signal-subscription APIs
accept the clearer `route_settings=` keyword. Runtime metrics expose
`RouteObservability`, `RouteLatencyMeasurement`, `RouteDeliveryMetrics`, and
`SignalQueueMetrics`.

Existing `EdgeContract`, `EdgeObservabilityLevel`, `RouteLatencyBoundary`,
`EdgeMetrics`, `TypedEdgeMetrics`, and `edge=` uses remain compatible throughout
the 0.1.x series.

## 0.1.2 — 2026-09-01

Capture code can now send source-aware audio to an external system through one
Expand Down Expand Up @@ -60,7 +74,7 @@ python -m pip install --upgrade pocketstation==0.1.2
```

Python Connectors run outside native realtime partitions, but each frame still
crosses the Python interpreter boundary. Use a native Connector when that cost
enters the Python interpreter. Use a native Connector when that cost
is not appropriate for the destination.

## 0.1.1 — 2026-09-01
Expand Down Expand Up @@ -98,7 +112,7 @@ does not execute on native capture callbacks.
### Voice interruption example

`examples/debug_voice_ai.py` connects a physical microphone directly to OpenAI
Realtime, routes generated speech through the normal Session audio path, and
Realtime, routes generated speech through the normal Session audio stream, and
records microphone input, generated output, and browser playback separately.
The resulting timeline distinguishes provider cancellation from Core output
cancellation and receiver delivery.
Expand All @@ -110,7 +124,7 @@ provider-history truncation as unavailable instead of inferring them.
### Supported and qualified environments

- macOS Apple silicon has installed-wheel evidence for application capture,
physical microphone input, the 10 ms voice path, Relay, Chromium, and
physical microphone input, the 10 ms voice configuration, Relay, Chromium, and
multistem recording.
- Linux and Windows have Core application-selection and 10 ms capture evidence.
Installed Python distributions are qualified separately by the release
Expand Down
6 changes: 5 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ need the lower-level API.
## Create an integration

- [Write application-owned audio into a Session](guides/application-audio.md)
- [Process audio and typed signals](guides/process-audio-and-signals.md)
- [Record stems and inspect Session delivery](guides/record-and-observe.md)
- [Compose a bounded voice workflow](guides/voice.md)
- [Publish a named AudioBus through Relay](guides/relay.md)
Expand All @@ -21,6 +22,8 @@ need the lower-level API.
## Understand the system

- [Session ownership, bounds, and shutdown](concepts/session-and-bounds.md)
- [Source identity, timestamps, and discontinuities](concepts/source-identity-and-time.md)
- [Media and delivery settings for each route](concepts/route-settings.md)

## Operate and upgrade

Expand All @@ -35,13 +38,14 @@ need the lower-level API.
- [`pocketstation.aio`](../python/pocketstation/aio/__init__.py) — asyncio over
the same native Session.
- [`pocketstation.voice`](../python/pocketstation/voice/__init__.py) —
provider-neutral voice composition contracts.
provider-neutral voice composition protocols.
- [`pocketstation.graph`](../python/pocketstation/graph.py) — stems, routes,
ports, and signals.
- [`pocketstation.observations`](../python/pocketstation/observations.py) —
runtime metrics and outcomes.
- [Python API map](reference/api-map.md) — public entry points and advanced
modules by task.
- [Events, metrics, outcomes, and errors](reference/events-and-errors.md)

For supported platforms and current qualification limits, read the
[repository README](../README.md#platform-support).
72 changes: 72 additions & 0 deletions docs/concepts/route-settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Set media and delivery behavior for a route

Every destination receives data through its own finite queue. PocketStation
chooses suitable settings for common capture, recording, Connector, and signal
subscription work. Use `RouteSettings` when an integration needs to state those
choices explicitly.

`RouteSettings` keeps two decisions together without mixing their APIs:

- `MediaCaps` describes the media accepted by the route.
- `DeliveryPolicy` describes timing, queue pressure, loss, copying, and
observations.

## Use a preset first

Realtime PCM should keep capture moving when one destination falls behind:

```python
from pocketstation.graph import RouteSettings

settings = RouteSettings.realtime_audio()
```

Typed signals use a finite asynchronous queue:

```python
settings = RouteSettings.bounded_async()
```

Connected ports still negotiate the concrete sample rate, channel layout, and
signal schema.

## Change delivery without changing media

```python
from pocketstation.graph import (
BackpressurePolicy,
DeliveryPolicy,
RouteSettings,
)

delivery = DeliveryPolicy.realtime_audio().with_backpressure(
BackpressurePolicy.DROP_OLDEST
)
settings = RouteSettings.realtime_audio().with_delivery_policy(delivery)
```

Pass the result to a Connector destination:

```python
destination = session.destination(connector, route_settings=settings)
application.send(destination)
```

Use `DROP_OLDEST` only when fresh realtime media matters more than queued
media. Use the default `DROP_NEWEST` when preserving items already accepted by
the destination is more important. A blocking policy is not valid on a capture
callback or realtime worker.

## Read what the Session compiled

Advanced Connector and Endpoint preparation objects expose `route_settings`.
`RouteMetrics.delivery` returns `RouteDeliveryMetrics` for queue depth,
attempted delivery, drops, and discontinuities. Typed signals use
`SignalQueueMetrics`. Check those observations before increasing capacity: a
larger finite queue can hold older audio without solving the slow destination.

`EdgeContract` remains an import-compatible name for `RouteSettings` in the
0.1.x series. Existing `edge=` keyword arguments continue to work. New code
should use `route_settings=` so the decision is clear at the call site.
`EdgeMetrics` and `TypedEdgeMetrics` remain compatibility names for the clearer
metrics types.
4 changes: 2 additions & 2 deletions docs/concepts/session-and-bounds.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ destinations does not mix that identity or recapture the source.
## Every crossing is finite

Audio input, polling, Python provider work, signals, Relay, and recording use
declared capacities. When a boundary is full, PocketStation returns or records
declared capacities. When a queue is full, PocketStation returns or records
pressure according to that route's policy. It does not hide pressure in an
unbounded `asyncio.Queue`.

Expand All @@ -36,7 +36,7 @@ bounded off-realtime workers. Native capture callbacks remain allocation-free,
lock-free, blocking-free, async-free, log-free, and panic-free.

Use a compiled extension when code must stay native. Use a process sidecar when
crash isolation matters. Neither boundary creates a second Session engine.
crash isolation matters. Neither option creates a second Session engine.

## Stop and cancel are different

Expand Down
71 changes: 71 additions & 0 deletions docs/concepts/source-identity-and-time.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Keep each source identifiable

PocketStation keeps application audio, microphone audio, and application-owned
PCM separate even when they use the same Operator, Connector, RelaySession, or
recording. A frame does not become anonymous when it leaves capture.

## Read the identities carried by a frame

`AudioFrame` exposes the identifiers needed to correlate live delivery with
provider observations and recorded stems:

| Value | Meaning |
|---|---|
| `source_id` | The Source instance that produced the media. |
| `stream_id` | One output stream declared by that Source. |
| `stem_id` | The independently routed and recorded audio stem. |
| `sequence_number` | The frame order within the current source generation. |
| `timestamp_start_ns` | The first sample time in its declared clock domain. |
| `source_generation` | The current attachment of a recoverable Source. |
| `discontinuity_epoch` | A change that prevents the timeline from being treated as continuous. |

Use these values together. A display name such as `Zoom` helps a person choose
an application; it is not a media identity.

## Select an application before capture starts

The concise API accepts an exact display name, an application identifier, or a
positive process ID:

```python
import pocketstation as pks
from pocketstation.sources import SourceQuery

with pks.capture(application="Zoom") as live:
for frame in live.audio:
print(frame.source_id, frame.stream_id, frame.sequence_number)
```

PocketStation rejects zero matches and ambiguous matches. It does not select
the first process silently.

Use discovery when the application must display choices or remember one:

```python
matches = pks.discover_sources(SourceQuery.application("Zoom"))
if len(matches) != 1:
raise RuntimeError("Select one running Zoom source")

selected = matches[0]
source = pks.Source.from_discovered(selected)
```

Store the discovered selector only for its reported persistence scope. A
process ID ends with that process. A platform application identity may survive
a normal restart, but should still be resolved again before a new Session.

## Treat discontinuity as data

A sequence gap, timestamp gap, source restart, or dropped route frame changes
what downstream code may infer. PocketStation records those changes instead of
inserting silent continuity.

An Operator can preserve the input lineage and add derivation for its output.
A Connector receives the source-aware `AudioFrame`. A recording manifest keeps
the same source and stem identity beside the WAV file.

Do not rewrite a missing timestamp as zero, merge two source generations, or
claim that a remote receiver played a sample from a sender timestamp alone.

Continue with [recording and observations](../guides/record-and-observe.md) or
[platform source persistence](../operations/platform-support.md).
2 changes: 1 addition & 1 deletion docs/getting-started/capture.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Install the package:
python -m pip install pocketstation
```

## Run the shortest capture path
## Capture an application

```python
import pocketstation
Expand Down
Loading
Loading