> ## Documentation Index
> Fetch the complete documentation index at: https://docs.synheart.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Providers

> BiosignalProvider and BehaviorProvider — the two pluggable data sources the engine consumes

The session SDK decouples the engine from data sources via two abstractions. This page covers the contracts, the bundled implementations, and the integration semantics with the engine.

## BiosignalProvider

The HR/HRV/RR source. Stream-based: the engine subscribes once per session and unsubscribes on stop.

### Contract (Dart)

```dart theme={null}
abstract class BiosignalProvider {
  bool get isAvailable;
  String get name;
  Stream<BiosignalSample> startStreaming();
  void stopStreaming();
}
```

Kotlin (`interface BiosignalProvider`) and Swift (`protocol BiosignalProvider`) mirror this. `name` is a debug identifier (not displayed to users). `isAvailable` is a self-report — the engine still subscribes when `false` if the host explicitly opts in.

### BiosignalSample

```dart theme={null}
class BiosignalSample {
  final int timestampMs;
  final double bpm;
  final double? rrIntervalMs;
  final ({double x, double y, double z})? accelerometer;  // record type
  final double? spo2;
}
```

Required: `timestampMs` (epoch ms), `bpm` (instantaneous heart rate). Optional: `rrIntervalMs` (single inter-beat interval), `accelerometer` (tri-axis, present on watch builds), `spo2` (when the wearable reports it).

### Engine consumption

`LiveSessionEngine` subscribes on `startSession` and pushes each sample into a fixed-capacity ring buffer sized at `windowSec`. There is no provider-side rate limiting; the engine drops oldest samples on overflow.

The accelerometer and SpO₂ fields are not consumed by `LiveSessionEngine` today — they are forwarded only when the engine emits `BiosignalFrame` events (raw streaming opt-in). The watch companions populate them at source.

### Bundled implementations

| Implementation                   | Package   | Notes                                                                  |
| -------------------------------- | --------- | ---------------------------------------------------------------------- |
| `MockBiosignalProvider`          | all three | Deterministic with seed; safe for CI.                                  |
| `WatchBiosignalProvider`         | Dart      | Bridges through `SessionChannel`. `isAvailable = true` optimistically. |
| `WearBiosignalProvider`          | Kotlin    | Reads from `synheart-wear` adapters.                                   |
| `HealthConnectBiosignalProvider` | Kotlin    | Reads from Health Connect on the phone.                                |
| HealthKit provider               | Swift     | Under `SynheartSessionHealthKit`.                                      |
| Wear (watchOS) provider          | Swift     | Under `SynheartSessionWear`.                                           |

External providers (host-supplied):

* BLE HRM: hosts can build a `BiosignalProvider` over `synheart-wear`'s BLE HRM client.
* Custom mocks for replay tests.

## BehaviorProvider

The behavioral signal source. Pull-based: the engine calls `currentSnapshot()` at each frame tick.

### Contract

```dart theme={null}
abstract class BehaviorProvider {
  bool get isAvailable;
  String get name;
  BehaviorSnapshot? currentSnapshot();
}
```

A `null` return means "no snapshot available right now"; the engine omits the `behavior` field on that frame.

### BehaviorSnapshot

```dart theme={null}
class BehaviorSnapshot {
  final int timestamp;            // epoch ms when captured

  // typing
  final double? typingCadence;
  final double? interKeyLatency;
  final int? burstLength;

  // scrolling
  final double? scrollVelocity;
  final double? scrollAcceleration;
  final double? scrollJitter;

  // taps and app context
  final double? tapRate;
  final int appSwitchesPerMinute;          // default 0, not nullable
  final double? foregroundDuration;
  final double? idleGapSeconds;

  // session-level indices
  final double? stabilityIndex;
  final double? fragmentationIndex;
}
```

The shape is **field-for-field aligned with `synheart_behavior`'s `BehaviorStats`** so the two SDKs share a contract without import dependencies. JSON serialisation uses snake\_case.

When emitted on a `SessionFrame.behavior` field, only the non-null fields appear (the `toJson` strips nulls except `appSwitchesPerMinute` and `timestamp`).

### Bundled implementations

| Implementation                      | Package             | Notes                                                                                                                        |
| ----------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `MockBehaviorProvider`              | all three           | Returns stable mid-range values for tests.                                                                                   |
| `synheart_behavior`-backed provider | Dart, Kotlin, Swift | Apps wire `BehaviorProvider` to a `synheart_behavior` `BehaviorEngine`; the snapshots come from the active behavior session. |

The session SDK does not bundle a default real-data behavior provider; that wiring lives in `synheart-core` (which depends on both packages).

## Integration with the engine

```text theme={null}
SessionConfig
  ├── biosignalProvider → engine subscribes for the duration of the session
  └── behaviorProvider  → engine pulls a snapshot at each emit_interval_sec
                           (and once more at SessionSummary)
```

Both providers are optional. With neither configured:

* `LiveSessionEngine` still emits `SessionStarted`, frames with `sample_count = 0`, `hr_mean_bpm = 0.0`, no behavior, and a final `SessionSummary`.
* This is the deterministic-no-data path useful for testing the framing logic itself.

## Provider state machine (informal)

```text theme={null}
provider constructed
    │ isAvailable = false?  → engine skips subscription entirely
    ▼
startStreaming() called
    │
    ▼
streaming
    │ stopStreaming() called  (engine on session end / dispose)
    ▼
stopped
```

Providers must tolerate `stopStreaming()` being called multiple times. They must stop emitting after `stopStreaming()` returns; samples emitted after the engine has unsubscribed are dropped silently.

## Watch provider specifics

`WatchBiosignalProvider`:

* `BiosignalFrame` events → emit each `BiosignalSample` directly to the controller.
* `SessionFrame` events → synthesise one sample from `metrics.hr_mean_bpm` with `rrIntervalMs = 60000.0 / bpm`.
* `SessionStarted`, `SessionSummary`, `SessionError` → ignored (lifecycle, not biosignal data).

Channel errors are non-fatal: the provider swallows them so the engine doesn't terminate the session on watch reconnect/disconnect.

## Authoring a provider

A custom `BiosignalProvider` should:

* Allocate any sensor resources (BLE GATT subscribe, HK authorization, etc.) on `startStreaming()`.
* Release them on `stopStreaming()`.
* Emit samples on the broadcast `Stream<BiosignalSample>` returned from `startStreaming()`.
* Not block the calling thread — the engine subscribes synchronously.

A custom `BehaviorProvider` should:

* Maintain its own internal accumulators.
* Return a fresh `BehaviorSnapshot` on each `currentSnapshot()` call (or `null` when no recent activity).
* Use real `DateTime.now().millisecondsSinceEpoch` for the snapshot timestamp.

## Related

* [Lifecycle](/synheart-session/lifecycle) — when the engine subscribes/unsubscribes.
* [Watch Protocol](/synheart-session/watch-protocol) — `BiosignalBatch` and `BiosignalSample` on the wire.
