Add proposal for agentic collaboration control plane and data plane

Create a new RFC document and associated JSON schemas defining an agentic control plane for automated insulin delivery systems, including event envelopes, capabilities models, delivery tracking, and conflict rules.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 06715f81-f852-4156-8ab0-cf9e0aa3564d
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 010cdc4a-2c86-4d2e-9768-bf2769a89953
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
bewest
2026-01-19 13:12:53 -08:00
committed by Ben West
parent 411e37ae15
commit 12c90cfc63
18 changed files with 3932 additions and 0 deletions
@@ -0,0 +1,151 @@
Below is a refinement / validation pass on your plan, with strong concurrence, notable gaps, and a question list for Trio / Loop / AAPS implementers.
Strong concurrence
• Separating config vs runtime vs computed state is exactly right
• ProfileDefinition / OverrideDefinition (user-authored, versioned)
• ProfileSelection / OverrideInstance (runtime intent)
• PolicyComposition (effective “whats in force”)
This is the cleanest way to make Nightscout a neutral control-plane and keep MDI as the always-valid fallback.
• Bridge mode from devicestatus is the correct migration strategy
• Synthesize canonical events from snapshots initially; let controllers move to native events over time.
• This preserves backward compatibility and lets Nightscout evolve without forcing every client to update immediately.
• Authority + audit trails as first-class concerns are non-negotiable
• Your requestedBy + authority fields are foundational for safe delegation/agents.
Whats missing / important to add
1) Youll want an explicit Event layer (even if you store objects too)
Right now the plan describes objects; it should explicitly define:
• Event envelope (append-only stream)
• eventId (stable UUID)
• eventType (e.g., profile.definition.upserted, override.instance.activated, policy.composed, delivery.requested)
• issuer (controller/user/agent id)
• issuerSeq (monotonic per issuer) or (issuer, time, nonce)
• cursor (server-assigned monotonic global ordering)
• idempotencyKey (for retries)
• refs (IDs/hashes referenced)
• payload (object snapshot or delta)
This is what makes cursor-based sync and reconciliation robust.
2) Capabilities need a real model (not just an optional pointer)
Your capabilitySnapshotId? is good, but youll likely need:
• ControllerKindDefinition (declared schema + what it supports)
• ControllerInstanceRegistration (this phone/controller right now)
• CapabilitySnapshot (what it can do now, including degraded states)
• pump connectivity status
• automation enabled/disabled
• max basal/bolus ceilings effective right now
• CGM health / confidence
• time sync health
Without this, “smart forms” and “digital twin honesty” get shaky.
3) Delivery tracking objects (you alluded, but not in the schema list)
To complete “intent vs reality” you want the minimal trio:
• DeliveryRequest (controller intent)
• DeliveryObservation (pump-confirmed reality)
• Reconciliation (match/partial/blocked/unknown + why)
This is also how you keep “taking a shot” present: an injection is a DeliveryObservation from a human source.
4) Conflict rules & multi-writer semantics
Nightscout will see inputs from:
• controller app
• caregiver app
• agent(s)
• manual UI
• bridge-synthesized from devicestatus
Define in core:
• How concurrent OverrideInstance are merged (composition is authoritative record)
• What “superseded” means across issuers
• Whether Nightscout allows multiple active overrides of same type
• How to prevent “flip-flop” loops (agent toggles override repeatedly)
5) Hashing/versioning rules need to be explicit
For ProfileDefinition:
• Canonicalization before hashing (sorted keys, normalized units/timezone)
• How to represent “same content” across clients
• Backward compatibility: allow “legacy profile name” mapping to hash/ID
6) Security / trust boundary (important for delegation)
At minimum:
• issuer identity model (API keys, OAuth identity, device identity)
• authority scopes (“override-only”, “suggest-only”, “can-approve”, “can-activate”)
• audit immutability expectations
Optional but valuable:
• signed events (controller signs with a device key)
• tamper-evidence (hash chain per issuer)
7) WebSockets: good idea, but make it optional
Real-time subscriptions are helpful, but:
• Start with cursor polling and/or SSE
• Add WebSockets after the event contract stabilizes
This reduces operational complexity early.
Questions to answer with Trio / Loop / AAPS going forward
These are the key “integration truth” questions that determine how cleanly they can emit native events and how well bridge mode works.
A) Profiles & overrides semantics
1. Do you have stable identifiers for a profile (beyond name)? If not, can you emit a content hash?
2. Can you represent overrides as:
• “template” (definition) vs “activation” (instance)?
3. What override dimensions exist today?
• target range vs single target
• sensitivity multiplier / autosens ratio
• carb ratio multiplier
• basal multiplier / max basal
4. How do you resolve multiple overrides? (precedence rules)
B) Composition
5. Do you compute an explicit “effective therapy settings now” object internally?
6. Can you emit:
• inputs referenced (profile hash + active override IDs)
• effective parameters (target range, effective ISF/CR adjustments, safety ceilings)
• controller version/build
If they can emit #5#6, Nightscouts digital twin becomes reliable without simulating their algorithm.
C) Delivery fidelity
7. Can you distinguish clearly between:
• suggested/recommended action
• requested command sent to pump
• pump-confirmed enactment
8. Do you have pump ACK/NAK/error codes that can be surfaced?
9. How do you represent “capped by limits” vs “could not enact due to comms”?
D) Timing and ordering
10. Can you provide monotonic ordering per controller? (sequence numbers)
If not, what clock guarantees exist?
11. How do you handle offline batching / delayed uploads?
E) Minimal payload commitment
12. What is the smallest “native event set” youre willing to emit first?
• override activated/ended
• policy composed
• delivery observed summary
This determines Phase 1 adoption feasibility.
Net: plan is solid, but to be “complete” add these core pieces
If you add (1) Event envelope, (2) Capabilities model, and (3) Delivery request/observe/reconcile, you have a full, coherent control-plane that:
• stays neutral
• supports smart forms / CRD-like extensibility
• preserves MDI as first-class
• enables agents safely
If you want, I can turn this into a tight Phase 13 implementation plan with:
• collections/endpoints,
• bridge rules from devicestatus,
• and the minimal JSON Schemas (OverrideInstance + PolicyComposition + EventEnvelope) as actual draft-2020-12 schema files.
+804
View File
@@ -0,0 +1,804 @@
# RFC: Agentic Control Plane for Automated Insulin Delivery Systems
**Status:** Draft
**Authors:** Nightscout Community
**Created:** 2026-01-01
**Last Updated:** 2026-01-01
---
## Abstract
This RFC proposes a clean separation between **control plane** (policy, configuration, intent) and **data plane** (observations, telemetry, delivery) for Nightscout and compatible automated insulin delivery (AID) systems like Loop, Trio, and AAPS.
The goal is to enable **agentic collaboration**—where AI agents, caregivers, and automation systems can safely participate in therapy management alongside the primary controller—while maintaining MDI (manual insulin delivery) as an always-valid fallback.
---
## Table of Contents
1. [Motivation](#motivation)
2. [Design Principles](#design-principles)
3. [Architecture Overview](#architecture-overview)
4. [Core Data Model](#core-data-model)
- [Event Envelope](#event-envelope)
- [Configuration Objects](#configuration-objects)
- [Runtime State Objects](#runtime-state-objects)
- [Computed State Objects](#computed-state-objects)
- [Delivery Tracking Objects](#delivery-tracking-objects)
- [Capabilities Model](#capabilities-model)
5. [API Design](#api-design)
6. [Bridge Mode: Legacy Compatibility](#bridge-mode-legacy-compatibility)
7. [Multi-Writer Semantics & Conflict Resolution](#multi-writer-semantics--conflict-resolution)
8. [Security & Authority Model](#security--authority-model)
9. [Implementation Phases](#implementation-phases)
10. [Integration Questions for Loop/AAPS/Trio](#integration-questions-for-loopaapstrio)
11. [Appendix: JSON Schemas](#appendix-json-schemas)
---
## Motivation
### Current State Problems
1. **Profiles as monolithic blobs** — Entire profile uploaded on every change; no versioning, content-hashing, or stable identifiers
2. **Overrides buried in devicestatus** — Temporary adjustments embedded in controller snapshots rather than discrete, auditable events
3. **No "effective policy" view** — No materialized representation of "what parameters are actually in force right now"
4. **Implicit authority** — No distinction between human intent, controller automation, and delegated agent actions
5. **Intent vs. reality gap** — Difficult to distinguish suggested actions from requested commands from confirmed delivery
### Why This Matters for Agents
For AI agents to safely assist with therapy management (reconciling hormone cycles, activity levels, geolocation, stress indicators), they need:
- **Clear authority boundaries** — What can an agent suggest vs. activate?
- **Audit trails** — Who changed what, when, and why?
- **Composable overrides** — Layer multiple adjustments without conflicts
- **Real-time policy state** — What's actually in force right now?
- **Delivery verification** — Did the suggested action actually happen?
---
## Design Principles
1. **Config vs. Runtime vs. Computed** — Separate user-authored configuration from runtime activations from computed effective state
2. **Events over Snapshots** — Append-only event streams with cursor-based sync, not mutable state blobs
3. **MDI as First-Class** — Manual injections are DeliveryObservations from a human source; the system never assumes automation
4. **Authority Hierarchy** — Human > Agent > Controller for conflict resolution
5. **Bridge Compatibility** — Synthesize canonical events from legacy devicestatus uploads
6. **Neutral Control Plane** — Nightscout stores intent and policy; controllers execute
---
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ NIGHTSCOUT │
│ (Neutral Control Plane) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ ┌───────────────────┐ │
│ │ CONFIG OBJECTS │ │ RUNTIME EVENTS │ │ COMPUTED STATE │ │
│ │ │ │ │ │ │ │
│ │ • ProfileDefinition │ │ • ProfileSelection │ │ • PolicyCompos- │ │
│ │ • OverrideDefinition │ │ • OverrideInstance │ │ ition │ │
│ │ • ControllerKind │ │ • DeliveryRequest │ │ • CapabilitySnap- │ │
│ │ Definition │ │ • DeliveryObserv- │ │ shot │ │
│ │ │ │ ation │ │ │ │
│ └──────────────────────┘ │ • Reconciliation │ └───────────────────┘ │
│ └──────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ EVENT STREAM │ │
│ │ cursor-ordered, append-only, per-issuer sequencing │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────────────┤
│ DATA PLANE │
│ ┌──────────────────────┐ ┌──────────────────────┐ ┌───────────────────┐ │
│ │ ENTRIES │ │ TREATMENTS │ │ DEVICESTATUS │ │
│ │ (CGM readings) │ │ (carbs, insulin) │ │ (legacy blob) │ │
│ └──────────────────────┘ └──────────────────────┘ └───────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ LOOP │ │ TRIO │ │ AAPS │
│ (Controller) │ │ (Controller) │ │ (Controller) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ AI AGENT │ │ CAREGIVER │ │ HUMAN │
│ (delegated) │ │ (remote) │ │ (primary) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
---
## Core Data Model
### Event Envelope
All state changes are wrapped in an **Event Envelope** for consistent ordering, replay, and audit.
```yaml
EventEnvelope:
eventId: string # Stable UUID
eventType: string # e.g., "profile.definition.created", "override.instance.activated"
cursor: integer # Server-assigned monotonic global ordering
issuer: string # Controller/user/agent identifier
issuerSeq: integer # Monotonic sequence per issuer
idempotencyKey: string # For retry deduplication
timestamp: datetime # ISO 8601
refs: # Referenced object IDs/hashes
- refType: string
refId: string
payload: object # The actual event data
```
**Event Types:**
| Category | Event Types |
|----------|-------------|
| Profile | `profile.definition.created`, `profile.definition.updated`, `profile.selection.changed` |
| Override | `override.definition.created`, `override.instance.activated`, `override.instance.ended`, `override.instance.superseded` |
| Policy | `policy.composition.computed` |
| Delivery | `delivery.requested`, `delivery.observed`, `delivery.reconciled` |
| Capability | `controller.registered`, `capability.snapshot.updated` |
---
### Configuration Objects
These are **user-authored, versioned, addressable** objects.
#### ProfileDefinition
```yaml
ProfileDefinition:
profileId: string # Stable identifier
contentHash: string # SHA-256 of canonicalized content
title: string # Human-readable name
timezone: string # IANA timezone
units: "mg/dL" | "mmol/L"
schedules:
basal: # Time-based basal rates
- time: "HH:MM"
rate: number # U/hr
isf: # Insulin sensitivity factor
- time: "HH:MM"
value: number
cr: # Carb ratio
- time: "HH:MM"
value: number
target: # Target glucose ranges
- time: "HH:MM"
low: number
high: number
insulinModel:
type: "rapid" | "fiasp" | "lyumjev" | "custom"
dia: number # Duration of insulin action (hours)
peakTime: number # Minutes to peak
createdBy:
issuerType: "human" | "controller" | "agent"
issuerId: string
createdAt: datetime
legacyProfileName: string # For backward compatibility mapping
```
#### OverrideDefinition
```yaml
OverrideDefinition:
definitionId: string
overrideType: "exercise" | "sleep" | "preMeal" | "illness" | "highActivity" | "hormones" | "custom"
title: string
defaultDuration: integer # seconds, null = indefinite
effects:
targetRange:
low: number
high: number
targetDelta: number # mg/dL adjustment to existing target
basalMultiplier: number # 1.0 = no change, 0.5 = 50%
maxBasalCeiling: number # U/hr cap
sensitivityMultiplier: number
carbRatioMultiplier: number
automationAggressiveness: number # 0.0 - 1.0 if controller supports
createdBy:
issuerType: "human" | "controller" | "agent"
issuerId: string
createdAt: datetime
```
---
### Runtime State Objects
These represent **concrete activations and intents**.
#### ProfileSelection
```yaml
ProfileSelection:
selectionId: string
selectedProfileId: string
selectedProfileHash: string # For verification
effectiveAt: datetime
selectedBy:
issuerType: "human" | "controller" | "agent"
issuerId: string
authority: "primary" | "delegated" | "automated"
reason: string # Optional annotation
```
#### OverrideInstance
```yaml
OverrideInstance:
instanceId: string
definitionId: string # Optional - may be ad-hoc
start: datetime
duration: integer # seconds, null = indefinite
end: datetime # Computed or explicit
effectiveEffects: # Resolved effects (may differ from definition)
targetRange:
low: number
high: number
basalMultiplier: number
sensitivityMultiplier: number
carbRatioMultiplier: number
requestedBy:
issuerType: "human" | "controller" | "agent"
issuerId: string
authority: "primary" | "delegated" | "automated"
status: "active" | "ended" | "canceled" | "superseded"
supersededBy: string # instanceId of superseding override
reason: string # Why this override was activated
annotations: object # Extensible metadata
```
---
### Computed State Objects
These are **materialized views** computed by Nightscout from events.
#### PolicyComposition
```yaml
PolicyComposition:
compositionId: string
references:
profileId: string
profileHash: string
activeOverrideInstanceIds: [string]
capabilitySnapshotId: string
effectiveParameters:
targetRange:
low: number
high: number
effectiveISF: number
effectiveCR: number
effectiveBasal: number # Current scheduled rate after multipliers
maxBasalAllowed: number
maxBolusAllowed: number
automationEnabled: boolean
computedBy:
controllerKind: string
controllerVersion: string
computedAt: datetime
validFrom: datetime
validTo: datetime # null = current
cursor: integer # For ordering
```
---
### Delivery Tracking Objects
These complete the **intent → action → confirmation** loop.
#### DeliveryRequest
```yaml
DeliveryRequest:
requestId: string
requestType: "tempBasal" | "bolus" | "suspend" | "resume"
parameters:
rate: number # U/hr for temp basal
units: number # Units for bolus
duration: integer # seconds for temp basal
requestedBy:
issuerType: "human" | "controller" | "agent"
issuerId: string
basedOn:
policyCompositionId: string
algorithmSuggestion: object # Optional: the suggestion that led to this
requestedAt: datetime
expiresAt: datetime # Request is stale after this
```
#### DeliveryObservation
```yaml
DeliveryObservation:
observationId: string
observationType: "basal" | "bolus" | "suspend" | "injection" | "pen"
source:
sourceType: "pump" | "manual" | "pen" | "inhaler"
sourceId: string # Device identifier
sourceKind: string # "omnipod" | "medtronic" | "tandem" | "pen"
observed:
rate: number
units: number
duration: integer
startTime: datetime
endTime: datetime
confidence: "confirmed" | "inferred" | "reported"
reportedBy:
issuerType: "human" | "controller" | "agent"
issuerId: string
observedAt: datetime
pumpResponse:
acked: boolean
errorCode: string
errorMessage: string
```
#### Reconciliation
```yaml
Reconciliation:
reconciliationId: string
requestId: string # The DeliveryRequest
observationId: string # The DeliveryObservation
outcome: "matched" | "partial" | "blocked" | "unknown" | "expired"
discrepancy:
requestedUnits: number
deliveredUnits: number
delta: number
reason: string # "capped_by_limit" | "comm_failure" | "user_canceled" | "pump_error"
reconciledAt: datetime
reconciledBy:
issuerType: "controller" | "agent"
issuerId: string
```
---
### Capabilities Model
For "digital twin honesty"—knowing what the controller can actually do right now.
#### ControllerKindDefinition
```yaml
ControllerKindDefinition:
kindId: string # "loop" | "trio" | "aaps" | "openaps"
version: string
supportedFeatures:
tempBasal: boolean
microBolus: boolean
suspend: boolean
overrides: boolean
autoSens: boolean
dynamicISF: boolean
dynamicCR: boolean
smbWithCOB: boolean
uam: boolean
supportedPumps: [string]
supportedCGMs: [string]
eventCapabilities:
canEmitNativeEvents: boolean
minimalEventSet: [string] # Event types it can emit
```
#### ControllerInstanceRegistration
```yaml
ControllerInstanceRegistration:
instanceId: string
kindId: string
version: string
device:
deviceId: string
platform: "ios" | "android" | "linux"
model: string
pumpBinding:
pumpKind: string
pumpSerial: string
connectedSince: datetime
cgmBinding:
cgmKind: string
cgmId: string
registeredAt: datetime
lastSeenAt: datetime
```
#### CapabilitySnapshot
```yaml
CapabilitySnapshot:
snapshotId: string
controllerInstanceId: string
connectivity:
pumpConnected: boolean
pumpLastContact: datetime
cgmConnected: boolean
cgmLastReading: datetime
automationState:
closedLoopEnabled: boolean
suspended: boolean
suspendReason: string
effectiveLimits:
maxBasal: number
maxBolus: number
maxIOB: number
health:
reservoirUnits: number
batteryPercent: number
cgmCalibrationStatus: string
timeSyncHealth: "good" | "drift" | "unknown"
snapshotAt: datetime
```
---
## API Design
### New Collections (API v3)
| Collection | Operations | Description |
|------------|------------|-------------|
| `/profileDefinitions` | CRUD + history | User-authored profile configs |
| `/profileSelections` | CRUD + history | Profile activation events |
| `/overrideDefinitions` | CRUD + history | Reusable override templates |
| `/overrideInstances` | CRUD + history | Concrete override activations |
| `/policyCompositions` | Read + history | Computed effective policy (read-only) |
| `/deliveryRequests` | CRUD + history | Delivery intent records |
| `/deliveryObservations` | CRUD + history | Confirmed delivery records |
| `/reconciliations` | Read + history | Request/observation matching |
| `/controllerRegistrations` | CRUD + history | Controller instance registry |
| `/capabilitySnapshots` | CRUD + history | Controller capability state |
| `/events` | Read + subscribe | Unified event stream |
### Event Stream Endpoint
```
GET /api/v3/events?cursor={lastCursor}&eventTypes={types}&issuers={ids}
```
Returns events after the given cursor, optionally filtered by type and issuer.
### WebSocket / SSE Subscriptions
```
WS /api/v3/events/subscribe
SSE /api/v3/events/stream?cursor={cursor}
```
Real-time event delivery with cursor-based resumption.
---
## Bridge Mode: Legacy Compatibility
For controllers that continue uploading `devicestatus` blobs, Nightscout synthesizes canonical events.
### Bridge Processing Pipeline
```
devicestatus upload
┌──────────────────────┐
│ Parse devicestatus │
│ • Extract profile │
│ • Extract overrides │
│ • Extract pump │
│ • Extract suggested │
│ • Extract enacted │
└──────────────────────┘
┌──────────────────────┐
│ Diff against last │
│ known state │
└──────────────────────┘
┌──────────────────────┐
│ Emit events: │
│ • ProfileDefinition │ (if profile content hash changed)
│ • ProfileSelection │ (if active profile changed)
│ • OverrideInstance │ (if override state changed)
│ • PolicyComposition │ (always, as snapshot)
│ • DeliveryObserv- │ (if enacted present)
│ ation │
└──────────────────────┘
┌──────────────────────┐
│ Store devicestatus │
│ with bridge refs │
└──────────────────────┘
```
### Bridge Rules
1. **Profile hashing:** Canonicalize (sort keys, normalize units/timezone), then SHA-256
2. **Override diffing:** Compare active override state; emit `activated`/`ended` as needed
3. **Delivery extraction:** Map `enacted` to `DeliveryObservation`, `suggested` to metadata
4. **Idempotency:** Use `(issuer, issuerSeq)` or `(devicestatus._id, field)` for dedup
---
## Multi-Writer Semantics & Conflict Resolution
### Writers
Nightscout accepts inputs from:
- Controller app (primary automation)
- Caregiver app (remote monitoring/intervention)
- AI agents (delegated assistance)
- Manual UI (user-initiated)
- Bridge (synthesized from legacy uploads)
### Authority Hierarchy
```
HUMAN (primary)
├── HUMAN (caregiver, delegated)
├── AGENT (delegated)
└── CONTROLLER (automated)
```
### Conflict Rules
1. **Override composition:** Multiple active overrides are composed into PolicyComposition; conflicts resolved by:
- Most restrictive target range
- Lowest basal multiplier (safety bias)
- Human > Agent > Controller authority
2. **Supersession:** A new override of the same type from equal or higher authority supersedes the previous
3. **Flip-flop prevention:**
- Rate limiting per issuer (max N changes per time window)
- Cooldown period after override end before same type can be activated
- Agent-initiated overrides require human confirmation if >N in period
4. **Profile selection:** Most recent selection wins; PolicyComposition always references current selection
---
## Security & Authority Model
### Identity Model
| Issuer Type | Identity Mechanism |
|-------------|-------------------|
| Human | OAuth identity (Nightscout account) |
| Controller | Device-bound API key + device attestation |
| Agent | OAuth + scoped delegation token |
| Caregiver | OAuth + explicit delegation grant |
### Authority Scopes
```yaml
Scopes:
read:
- entries.read
- treatments.read
- policy.read
- delivery.read
suggest:
- override.suggest # Can propose, human must approve
- delivery.suggest
activate:
- override.activate # Can directly activate
- profile.select
deliver:
- delivery.request # Can issue delivery requests
admin:
- controller.register
- delegation.grant
```
### Delegation Model
```yaml
DelegationGrant:
grantId: string
grantedBy: string # Human issuer ID
grantedTo: string # Agent/caregiver issuer ID
scopes: [string]
constraints:
maxOverrideDuration: integer
allowedOverrideTypes: [string]
requireConfirmation: boolean
expiresAt: datetime
grantedAt: datetime
revokedAt: datetime
```
### Audit Requirements
1. All events are append-only
2. Events include `issuer`, `authority`, `timestamp`
3. Optional: issuer-signed events with device keys
4. Optional: hash chain per issuer for tamper evidence
---
## Implementation Phases
### Phase 1: Foundation (4-6 weeks)
**Goal:** Core event model and minimal collections
- [ ] EventEnvelope schema and storage
- [ ] ProfileDefinition collection with content hashing
- [ ] OverrideInstance collection
- [ ] PolicyComposition collection (computed)
- [ ] Basic bridge from devicestatus
- [ ] Cursor-based event polling endpoint
- [ ] SSE subscription (before WebSocket)
**Deliverables:**
- New API v3 collections operational
- Legacy devicestatus → event synthesis working
- Clients can poll for events
### Phase 2: Delivery & Capabilities (4-6 weeks)
**Goal:** Complete intent-to-delivery loop
- [ ] DeliveryRequest / DeliveryObservation / Reconciliation
- [ ] ControllerKindDefinition / ControllerInstanceRegistration
- [ ] CapabilitySnapshot
- [ ] Enhanced bridge for delivery extraction
- [ ] WebSocket subscriptions
**Deliverables:**
- Full delivery tracking operational
- Controller capability awareness
- Real-time subscriptions
### Phase 3: Agents & Delegation (6-8 weeks)
**Goal:** Safe multi-writer with agents
- [ ] Authority scopes and delegation grants
- [ ] Agent identity and authentication
- [ ] Conflict resolution rules
- [ ] Rate limiting and flip-flop prevention
- [ ] Confirmation workflows for agent suggestions
- [ ] Audit dashboard
**Deliverables:**
- Agents can suggest overrides
- Caregivers can delegate to agents
- Full audit trail
---
## Integration Questions for Loop/AAPS/Trio
See [integration-questionnaire.md](./integration-questionnaire.md) for the complete questionnaire.
### Key Questions Summary
**A) Profiles & Overrides**
1. Do you have stable profile identifiers beyond name?
2. Can you represent overrides as template vs. activation?
3. What override dimensions exist (target, sensitivity, basal, CR)?
4. How do you resolve multiple concurrent overrides?
**B) Composition**
5. Do you compute an explicit "effective therapy settings" internally?
6. Can you emit: profile hash + override IDs + effective parameters?
**C) Delivery Fidelity**
7. Can you distinguish suggested vs. requested vs. confirmed?
8. Do you surface pump ACK/NAK/error codes?
9. How do you represent "capped by limits" vs. "comm failure"?
**D) Timing & Ordering**
10. Can you provide monotonic per-controller sequencing?
11. How do you handle offline batching?
**E) Minimal Commitment**
12. What's the smallest native event set you'd emit first?
---
## Appendix: JSON Schemas
Full JSON Schema (draft-2020-12) files are available in the `schemas/` directory:
- [event-envelope.schema.json](./schemas/event-envelope.schema.json)
- [profile-definition.schema.json](./schemas/profile-definition.schema.json)
- [profile-selection.schema.json](./schemas/profile-selection.schema.json)
- [override-definition.schema.json](./schemas/override-definition.schema.json)
- [override-instance.schema.json](./schemas/override-instance.schema.json)
- [policy-composition.schema.json](./schemas/policy-composition.schema.json)
- [delivery-request.schema.json](./schemas/delivery-request.schema.json)
- [delivery-observation.schema.json](./schemas/delivery-observation.schema.json)
- [reconciliation.schema.json](./schemas/reconciliation.schema.json)
- [controller-kind-definition.schema.json](./schemas/controller-kind-definition.schema.json)
- [controller-instance-registration.schema.json](./schemas/controller-instance-registration.schema.json)
- [capability-snapshot.schema.json](./schemas/capability-snapshot.schema.json)
---
## References
- [Nightscout API v3 Documentation](../api3/swagger.yaml)
- [Loop Documentation](https://loopkit.github.io/loopdocs/)
- [AAPS Documentation](https://androidaps.readthedocs.io/)
- [OpenAPS Documentation](https://openaps.readthedocs.io/)
---
## Changelog
| Date | Change |
|------|--------|
| 2026-01-01 | Initial draft |
+642
View File
@@ -0,0 +1,642 @@
# Bridge Mode: Legacy devicestatus to Event Synthesis
**Purpose:** Define rules for synthesizing canonical control plane events from legacy `devicestatus` uploads, enabling backward compatibility while controllers transition to native event emission.
---
## Overview
Bridge mode enables Nightscout to:
1. Accept legacy `devicestatus` uploads unchanged
2. Parse and extract control plane information
3. Synthesize canonical events (`ProfileDefinition`, `OverrideInstance`, `PolicyComposition`, etc.)
4. Store both the original devicestatus and synthesized events
5. Enable event-based queries and subscriptions
```
┌─────────────────────────────────────────────────────────────────┐
│ DEVICESTATUS UPLOAD │
│ POST /api/v1/devicestatus │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ BRIDGE PROCESSOR │
│ │
│ 1. Store original devicestatus │
│ 2. Identify controller type (loop, openaps, aaps) │
│ 3. Extract profile information │
│ 4. Extract override state │
│ 5. Extract delivery information │
│ 6. Compute diffs from last known state │
│ 7. Emit synthesized events │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ EVENT STREAM │
│ │
│ • ProfileDefinition (if hash changed) │
│ • ProfileSelection (if active profile changed) │
│ • OverrideInstance (start/end as needed) │
│ • PolicyComposition (always) │
│ • DeliveryObservation (if enacted present) │
│ • CapabilitySnapshot (if pump/cgm status present) │
└─────────────────────────────────────────────────────────────────┘
```
---
## Controller Detection
### Detection Rules
```javascript
function detectControllerType(devicestatus) {
if (devicestatus.loop) return 'loop';
if (devicestatus.openaps) return 'openaps';
if (devicestatus.pump && devicestatus.pump.source === 'AAPS') return 'aaps';
if (devicestatus.trio) return 'trio';
return 'unknown';
}
```
### Controller-Specific Parsers
Each controller type has a dedicated parser:
| Controller | Parser Module | Primary Fields |
|------------|--------------|----------------|
| Loop | `parsers/loop.js` | `loop.enacted`, `loop.predicted`, `loop.override` |
| OpenAPS | `parsers/openaps.js` | `openaps.suggested`, `openaps.enacted`, `openaps.iob` |
| AAPS | `parsers/aaps.js` | `pump`, `configuration` |
| Trio | `parsers/trio.js` | `trio.enacted`, `trio.override` |
---
## Profile Extraction & Hashing
### Canonicalization Rules
Before hashing, profiles must be canonicalized:
1. **Sort keys** alphabetically at all levels
2. **Normalize timezone** to IANA format
3. **Normalize units** to lowercase (`mg/dl``mg/dL`, `mmol/l``mmol/L`)
4. **Normalize times** to `HH:MM` format (zero-padded)
5. **Round numbers** to standard precision (rates: 3 decimals, ratios: 2 decimals)
6. **Remove null/undefined** fields
7. **Sort arrays** by time field
```javascript
function canonicalizeProfile(profile) {
const canonical = {
basal: sortByTime(profile.basal).map(b => ({
time: normalizeTime(b.time),
rate: round(b.rate, 3)
})),
isf: sortByTime(profile.sens || profile.isf).map(s => ({
time: normalizeTime(s.time),
value: round(s.value, 1)
})),
cr: sortByTime(profile.carbratio || profile.cr).map(c => ({
time: normalizeTime(c.time),
value: round(c.value, 1)
})),
target: sortByTime(profile.target_low ?
mergeTargets(profile.target_low, profile.target_high) :
profile.target
).map(t => ({
time: normalizeTime(t.time),
low: round(t.low, 0),
high: round(t.high, 0)
})),
dia: round(profile.dia, 1),
timezone: normalizeTimezone(profile.timezone),
units: normalizeUnits(profile.units)
};
return canonical;
}
function hashProfile(canonicalProfile) {
const json = JSON.stringify(canonicalProfile, Object.keys(canonicalProfile).sort());
return crypto.createHash('sha256').update(json).digest('hex');
}
```
### Profile Definition Emission
```javascript
function maybeEmitProfileDefinition(devicestatus, lastKnownState) {
const profile = extractProfile(devicestatus);
if (!profile) return null;
const canonical = canonicalizeProfile(profile);
const hash = hashProfile(canonical);
// Check if we've seen this profile before
if (lastKnownState.profileHashes.has(hash)) {
return null; // Already exists, no need to emit
}
return {
eventType: 'profile.definition.created',
payload: {
profileId: generateProfileId(hash),
contentHash: hash,
title: profile.profileName || 'Default',
timezone: canonical.timezone,
units: canonical.units,
schedules: {
basal: canonical.basal,
isf: canonical.isf,
cr: canonical.cr,
target: canonical.target
},
insulinModel: {
dia: canonical.dia
},
createdBy: {
issuerType: 'controller',
issuerId: devicestatus.device || 'unknown'
},
legacyProfileName: profile.profileName
},
metadata: {
bridgeSource: 'devicestatus',
bridgeSourceId: devicestatus._id
}
};
}
```
---
## Override State Extraction
### Loop Override Parsing
```javascript
function parseLoopOverride(devicestatus) {
const override = devicestatus.loop?.override;
if (!override) return null;
return {
active: override.active !== false,
name: override.name,
targetRange: override.currentCorrectionRange ? {
low: override.currentCorrectionRange.minValue,
high: override.currentCorrectionRange.maxValue
} : null,
basalMultiplier: override.multiplier,
sensitivityMultiplier: override.insulinSensitivityScaleFactor,
duration: override.duration, // seconds
startTime: override.startTime || devicestatus.created_at,
symbol: override.symbol
};
}
```
### AAPS Override Parsing
```javascript
function parseAAPSOverride(devicestatus) {
// AAPS uses "Temp Target" and "Profile Switch" differently
const tempTarget = devicestatus.configuration?.tempTarget;
const profileSwitch = devicestatus.configuration?.profileSwitch;
const overrides = [];
if (tempTarget && tempTarget.isValid) {
overrides.push({
type: 'tempTarget',
active: true,
targetRange: {
low: tempTarget.lowTarget,
high: tempTarget.highTarget
},
reason: tempTarget.reason,
duration: tempTarget.duration * 60 // minutes to seconds
});
}
if (profileSwitch && profileSwitch.percentage !== 100) {
overrides.push({
type: 'profilePercentage',
active: true,
basalMultiplier: profileSwitch.percentage / 100,
duration: profileSwitch.duration * 60
});
}
return overrides;
}
```
### Override Instance Emission
```javascript
function emitOverrideEvents(currentOverride, lastKnownOverride, issuer) {
const events = [];
// Detect override start
if (currentOverride?.active && !lastKnownOverride?.active) {
events.push({
eventType: 'override.instance.activated',
payload: {
instanceId: generateUUID(),
start: currentOverride.startTime,
duration: currentOverride.duration,
effectiveEffects: {
targetRange: currentOverride.targetRange,
basalMultiplier: currentOverride.basalMultiplier,
sensitivityMultiplier: currentOverride.sensitivityMultiplier
},
requestedBy: {
issuerType: 'controller',
issuerId: issuer,
authority: 'automated'
},
status: 'active',
reason: currentOverride.name || currentOverride.reason
}
});
}
// Detect override end
if (!currentOverride?.active && lastKnownOverride?.active) {
events.push({
eventType: 'override.instance.ended',
payload: {
instanceId: lastKnownOverride.instanceId,
status: 'ended',
endedBy: {
issuerType: 'controller',
issuerId: issuer
}
}
});
}
// Detect override change (supersession)
if (currentOverride?.active && lastKnownOverride?.active &&
overridesDiffer(currentOverride, lastKnownOverride)) {
const newInstanceId = generateUUID();
events.push({
eventType: 'override.instance.superseded',
payload: {
instanceId: lastKnownOverride.instanceId,
status: 'superseded',
supersededBy: newInstanceId
}
});
events.push({
eventType: 'override.instance.activated',
payload: {
instanceId: newInstanceId,
// ... same as above
}
});
}
return events;
}
```
---
## Policy Composition Synthesis
Always emit a PolicyComposition for every devicestatus, as it represents the current state.
```javascript
function synthesizePolicyComposition(devicestatus, controllerType, state) {
const profile = extractProfile(devicestatus);
const override = extractOverride(devicestatus, controllerType);
const limits = extractLimits(devicestatus, controllerType);
// Compute effective parameters
const scheduledBasal = getCurrentScheduledValue(profile.basal);
const basalMultiplier = override?.basalMultiplier || 1.0;
const effectiveBasal = scheduledBasal * basalMultiplier;
const scheduledTarget = getCurrentScheduledValue(profile.target);
const effectiveTarget = override?.targetRange || scheduledTarget;
return {
eventType: 'policy.composition.computed',
payload: {
compositionId: generateUUID(),
references: {
profileId: state.currentProfileId,
profileHash: state.currentProfileHash,
activeOverrideInstanceIds: state.activeOverrideIds,
capabilitySnapshotId: state.latestCapabilitySnapshotId
},
effectiveParameters: {
targetRange: effectiveTarget,
effectiveISF: getCurrentScheduledValue(profile.isf),
effectiveCR: getCurrentScheduledValue(profile.cr),
effectiveBasal: effectiveBasal,
scheduledBasal: scheduledBasal,
basalMultiplier: basalMultiplier,
maxBasalAllowed: limits.maxBasal,
maxBolusAllowed: limits.maxBolus,
maxIOB: limits.maxIOB,
automationEnabled: extractAutomationState(devicestatus),
automationMode: extractAutomationMode(devicestatus)
},
computedBy: {
controllerKind: controllerType,
controllerVersion: devicestatus.version || 'unknown',
computedAt: new Date().toISOString()
},
validFrom: devicestatus.created_at
}
};
}
```
---
## Delivery Observation Extraction
### Loop Enacted Parsing
```javascript
function parseLoopEnacted(devicestatus) {
const enacted = devicestatus.loop?.enacted;
if (!enacted || !enacted.received) return null;
const observations = [];
// Temp basal
if (enacted.rate !== undefined) {
observations.push({
observationType: 'tempBasal',
source: {
sourceType: 'pump',
sourceKind: devicestatus.pump?.pumpModel || 'unknown'
},
observed: {
rate: enacted.rate,
duration: enacted.duration * 60, // minutes to seconds
startTime: enacted.timestamp
},
confidence: 'confirmed',
relatedSuggestion: {
reason: enacted.reason,
eventualBG: enacted.eventualBG,
predBGs: enacted.predBGs
}
});
}
// Bolus (if present)
if (enacted.units) {
observations.push({
observationType: enacted.units < 1 ? 'microBolus' : 'bolus',
source: {
sourceType: 'pump',
sourceKind: devicestatus.pump?.pumpModel || 'unknown'
},
observed: {
units: enacted.units,
startTime: enacted.timestamp
},
confidence: 'confirmed'
});
}
return observations;
}
```
### OpenAPS Enacted Parsing
```javascript
function parseOpenAPSEnacted(devicestatus) {
const enacted = devicestatus.openaps?.enacted;
const suggested = devicestatus.openaps?.suggested;
if (!enacted) return null;
return {
observationType: 'tempBasal',
source: {
sourceType: 'pump',
sourceKind: devicestatus.pump?.pumpmanufacturer || 'unknown'
},
observed: {
rate: enacted.rate,
duration: enacted.duration * 60,
startTime: enacted.timestamp
},
confidence: 'confirmed',
pumpResponse: {
acked: true
},
relatedSuggestion: suggested ? {
reason: suggested.reason,
iob: devicestatus.openaps?.iob?.iob,
eventualBG: suggested.eventualBG
} : null
};
}
```
---
## Capability Snapshot Extraction
```javascript
function extractCapabilitySnapshot(devicestatus, controllerType) {
const pump = devicestatus.pump || devicestatus.uploaderBattery;
return {
eventType: 'capability.snapshot.updated',
payload: {
snapshotId: generateUUID(),
controllerInstanceId: devicestatus.device,
connectivity: {
pumpConnected: pump?.status?.bolusing !== undefined,
pumpLastContact: pump?.clock,
cgmConnected: devicestatus.cgm !== undefined,
cgmLastReading: devicestatus.cgm?.mills
},
automationState: {
closedLoopEnabled: extractClosedLoopState(devicestatus, controllerType),
suspended: pump?.status?.suspended || false,
lastLoopTime: devicestatus.loop?.timestamp || devicestatus.openaps?.suggested?.timestamp
},
health: {
reservoirUnits: pump?.reservoir,
batteryPercent: pump?.battery?.percent || devicestatus.uploaderBattery,
phoneBatteryPercent: devicestatus.uploaderBattery
},
snapshotAt: devicestatus.created_at
}
};
}
```
---
## Idempotency & Deduplication
### Idempotency Keys
For bridge-synthesized events, use deterministic idempotency keys:
```javascript
function generateIdempotencyKey(devicestatusId, eventType, subKey = '') {
return crypto
.createHash('sha256')
.update(`bridge:${devicestatusId}:${eventType}:${subKey}`)
.digest('hex')
.substring(0, 32);
}
```
### Deduplication Rules
1. **ProfileDefinition:** Dedupe by `contentHash` — same hash = same profile
2. **OverrideInstance:** Dedupe by `(start, effectiveEffects hash)` within tolerance window
3. **PolicyComposition:** Allow duplicates (they're snapshots), but optimize storage
4. **DeliveryObservation:** Dedupe by `(startTime, type, units/rate)` within 30-second window
---
## State Management
The bridge processor maintains per-device state:
```javascript
const deviceState = {
deviceId: string,
lastDevicestatusId: string,
lastProcessedAt: datetime,
// Profile state
currentProfileId: string,
currentProfileHash: string,
profileHashes: Set<string>, // All known profile hashes
// Override state
activeOverrides: [{
instanceId: string,
type: string,
startTime: datetime,
effectsHash: string
}],
// Capability state
latestCapabilitySnapshotId: string,
// Sequence tracking
issuerSeq: number
};
```
---
## Error Handling
### Parsing Failures
```javascript
function processBridgeWithFallback(devicestatus) {
try {
const events = processDevicestatus(devicestatus);
return { success: true, events };
} catch (error) {
console.error('Bridge parsing error:', error);
// Emit a minimal PolicyComposition with error annotation
return {
success: false,
events: [{
eventType: 'policy.composition.computed',
payload: {
compositionId: generateUUID(),
references: {},
effectiveParameters: {},
computedBy: {
controllerKind: 'unknown',
computedAt: new Date().toISOString()
},
validFrom: devicestatus.created_at
},
metadata: {
bridgeError: error.message,
bridgeSource: 'devicestatus',
bridgeSourceId: devicestatus._id
}
}]
};
}
}
```
### Missing Fields
Handle gracefully with defaults:
```javascript
const DEFAULTS = {
basalMultiplier: 1.0,
sensitivityMultiplier: 1.0,
automationEnabled: true,
confidence: 'inferred'
};
```
---
## Testing
### Test Cases
1. **Profile change detection** — Same profile should not emit new definition
2. **Override start/end** — Correctly detect transitions
3. **Override supersession** — Detect when override changes while active
4. **Delivery extraction** — Parse all enacted formats
5. **Clock skew** — Handle out-of-order devicestatus
6. **Idempotency** — Retry same devicestatus produces no duplicates
7. **Controller-specific parsing** — Each controller type parsed correctly
### Test Data
See `tests/bridge/fixtures/` for sample devicestatus payloads from each controller.
---
## Configuration
```javascript
const BRIDGE_CONFIG = {
enabled: true,
// Which controllers to bridge
controllers: ['loop', 'openaps', 'aaps', 'trio'],
// Event emission
emitProfileDefinitions: true,
emitOverrideInstances: true,
emitPolicyCompositions: true,
emitDeliveryObservations: true,
emitCapabilitySnapshots: true,
// Deduplication
dedupeWindowSeconds: 30,
// State persistence
stateStorageCollection: 'bridgeState',
stateExpiryDays: 7,
// Error handling
continueOnParseError: true,
logParseErrors: true
};
```
+523
View File
@@ -0,0 +1,523 @@
# Multi-Writer Semantics & Conflict Resolution
**Purpose:** Define rules for handling concurrent inputs from multiple sources (controllers, caregivers, agents, manual UI) to the Nightscout control plane.
---
## Overview
The control plane accepts inputs from multiple writer types, each with different authority levels and use cases. This document defines how conflicts are detected, resolved, and audited.
---
## Writer Types
### Primary Writers
| Writer Type | Description | Authority Level | Examples |
|-------------|-------------|-----------------|----------|
| **Human (Primary)** | The person with diabetes (PWD) or primary caregiver | Highest | User activating override in app |
| **Human (Caregiver)** | Delegated caregiver with explicit permissions | High | Parent adjusting child's settings remotely |
| **Agent** | AI/automated system with delegated authority | Medium | AI agent suggesting/activating sleep mode |
| **Controller** | AID algorithm on device | Base | Loop activating temp basal |
### Writer Identity
Each writer has a unique identity:
```yaml
IssuerIdentity:
issuerType: "human" | "controller" | "agent" | "caregiver" | "system"
issuerId: string # Unique identifier
authority: "primary" | "delegated" | "automated"
delegatedBy: string # If delegated, who granted authority
delegationScopes: [string] # What actions are permitted
```
---
## Authority Hierarchy
```
┌─────────────────────────────────────────┐
│ HUMAN (PRIMARY) │ ← Can do anything
│ Authority Level: 100 │
└───────────────────┬─────────────────────┘
┌───────────────────▼─────────────────────┐
│ HUMAN (CAREGIVER) │ ← Delegated by primary
│ Authority Level: 80 │
└───────────────────┬─────────────────────┘
┌───────────────────▼─────────────────────┐
│ AGENT │ ← Delegated by primary/caregiver
│ Authority Level: 50 │
└───────────────────┬─────────────────────┘
┌───────────────────▼─────────────────────┐
│ CONTROLLER │ ← Automated, follows policy
│ Authority Level: 30 │
└─────────────────────────────────────────┘
```
### Authority Rules
1. Higher authority can always override lower authority
2. Equal authority uses temporal precedence (last write wins)
3. Lower authority cannot override higher authority actions
4. Controller cannot override active human/agent overrides
---
## Conflict Scenarios
### Scenario 1: Override Supersession
**Situation:** Human starts "Exercise" override, then Agent tries to start "High Activity" override.
**Resolution:**
```javascript
function canSupersede(newOverride, existingOverride) {
const newAuthority = getAuthorityLevel(newOverride.requestedBy);
const existingAuthority = getAuthorityLevel(existingOverride.requestedBy);
if (newAuthority > existingAuthority) {
return { allowed: true, action: 'supersede' };
}
if (newAuthority === existingAuthority) {
// Same type? Supersede. Different type? May compose.
if (newOverride.type === existingOverride.type) {
return { allowed: true, action: 'supersede' };
} else {
return { allowed: true, action: 'compose' };
}
}
// Lower authority cannot supersede
return {
allowed: false,
reason: 'insufficient_authority',
requiredAuthority: existingAuthority
};
}
```
### Scenario 2: Concurrent Override Composition
**Situation:** Two different override types are active simultaneously.
**Resolution:** Compose effects with conservative (safe) combination:
```javascript
function composeOverrides(overrides) {
// Sort by authority (highest first), then by start time
const sorted = sortBy(overrides, ['-authority', 'start']);
const composed = {
targetRange: null,
basalMultiplier: 1.0,
sensitivityMultiplier: 1.0,
carbRatioMultiplier: 1.0,
maxBasalCeiling: Infinity
};
for (const override of sorted) {
const effects = override.effectiveEffects;
// Target: Use most restrictive (highest low, lowest high)
if (effects.targetRange) {
if (!composed.targetRange) {
composed.targetRange = { ...effects.targetRange };
} else {
composed.targetRange.low = Math.max(
composed.targetRange.low,
effects.targetRange.low
);
composed.targetRange.high = Math.min(
composed.targetRange.high,
effects.targetRange.high
);
}
}
// Basal multiplier: Use lowest (most conservative)
if (effects.basalMultiplier !== undefined) {
composed.basalMultiplier = Math.min(
composed.basalMultiplier,
effects.basalMultiplier
);
}
// Max basal ceiling: Use lowest
if (effects.maxBasalCeiling !== undefined) {
composed.maxBasalCeiling = Math.min(
composed.maxBasalCeiling,
effects.maxBasalCeiling
);
}
// Sensitivity: Compound multiply
if (effects.sensitivityMultiplier !== undefined) {
composed.sensitivityMultiplier *= effects.sensitivityMultiplier;
}
}
return composed;
}
```
### Scenario 3: Profile Switch During Active Override
**Situation:** Human switches profile while override is active.
**Resolution:**
- Profile switch proceeds
- Override remains active
- PolicyComposition recalculated with new profile + existing override
```javascript
function handleProfileSwitch(newProfileSelection, activeOverrides) {
// Profile switch always allowed at appropriate authority level
emitEvent('profile.selection.changed', newProfileSelection);
// Recalculate composition with new profile and existing overrides
const composition = computePolicyComposition(
newProfileSelection.selectedProfileId,
activeOverrides
);
emitEvent('policy.composition.computed', composition);
// Overrides remain active - their effects are relative to new profile
return composition;
}
```
### Scenario 4: Agent Flip-Flop Prevention
**Situation:** Agent activates/deactivates same override repeatedly.
**Resolution:** Rate limiting and cooldown periods.
```javascript
const FLIP_FLOP_CONFIG = {
maxActivationsPerHour: 4,
cooldownAfterEnd: 15 * 60, // 15 minutes in seconds
requireConfirmationAfter: 2 // After 2 activations, require human confirmation
};
function checkFlipFlopLimits(agentId, overrideType) {
const recentActivations = getRecentActivations(agentId, overrideType, 3600);
if (recentActivations.length >= FLIP_FLOP_CONFIG.maxActivationsPerHour) {
return {
allowed: false,
reason: 'rate_limit_exceeded',
retryAfter: calculateRetryTime(recentActivations)
};
}
const lastEnd = getLastOverrideEnd(agentId, overrideType);
if (lastEnd) {
const cooldownRemaining = FLIP_FLOP_CONFIG.cooldownAfterEnd -
(Date.now() - lastEnd) / 1000;
if (cooldownRemaining > 0) {
return {
allowed: false,
reason: 'cooldown_active',
retryAfter: cooldownRemaining
};
}
}
if (recentActivations.length >= FLIP_FLOP_CONFIG.requireConfirmationAfter) {
return {
allowed: 'pending_confirmation',
reason: 'requires_human_confirmation',
confirmationRequest: createConfirmationRequest(agentId, overrideType)
};
}
return { allowed: true };
}
```
### Scenario 5: Controller vs. Human Override Interaction
**Situation:** Human has active override; controller tries to adjust.
**Resolution:** Controller respects human override; can only work within its bounds.
```javascript
function controllerCanAdjust(controllerRequest, activeHumanOverride) {
// Controller cannot:
// - End human-initiated override
// - Exceed limits set by human override
// - Change target outside human-set range
if (activeHumanOverride) {
// Controller works within bounds
return {
allowed: true,
constraints: {
maxBasal: Math.min(
controllerRequest.maxBasal,
activeHumanOverride.effectiveEffects.maxBasalCeiling || Infinity
),
targetRange: activeHumanOverride.effectiveEffects.targetRange,
// Controller can micro-adjust within these bounds
canAdjustBasal: true,
canBolus: true
}
};
}
return { allowed: true, constraints: null };
}
```
---
## Concurrency Control
### Optimistic Locking
For updates to existing objects:
```javascript
async function updateWithOptimisticLock(collection, id, update, expectedVersion) {
const result = await db.collection(collection).findOneAndUpdate(
{
_id: id,
srvModified: expectedVersion
},
{
$set: update,
$inc: { version: 1 }
},
{ returnDocument: 'after' }
);
if (!result.value) {
throw new ConflictError('Version mismatch - document was modified');
}
return result.value;
}
```
### Event Ordering
All events receive a monotonic cursor for global ordering:
```javascript
async function assignEventCursor(event) {
// Atomic increment of global cursor
const counter = await db.collection('eventCursors').findOneAndUpdate(
{ _id: 'global' },
{ $inc: { cursor: 1 } },
{ upsert: true, returnDocument: 'after' }
);
event.cursor = counter.value.cursor;
return event;
}
```
---
## Audit Trail
### Conflict Events
When conflicts are detected and resolved, emit audit events:
```javascript
function emitConflictResolution(conflict) {
return {
eventType: 'conflict.resolved',
payload: {
conflictId: generateUUID(),
conflictType: conflict.type,
participants: conflict.participants.map(p => ({
issuerId: p.issuerId,
issuerType: p.issuerType,
authority: p.authority,
action: p.action,
timestamp: p.timestamp
})),
resolution: {
outcome: conflict.resolution.outcome,
winner: conflict.resolution.winner,
reason: conflict.resolution.reason,
appliedAt: new Date().toISOString()
}
}
};
}
```
### Authority Escalation
When lower authority is blocked:
```javascript
function emitAuthorityBlock(request, blocker) {
return {
eventType: 'authority.blocked',
payload: {
requestedAction: request.action,
requestedBy: request.issuer,
requestedAuthority: request.authority,
blockedBy: blocker.instanceId,
blockerAuthority: blocker.authority,
reason: 'insufficient_authority',
requiredAuthority: blocker.authority,
timestamp: new Date().toISOString()
}
};
}
```
---
## Delegation Grants
### Grant Structure
```javascript
const delegationGrant = {
grantId: 'uuid',
grantedBy: 'human-user-id',
grantedTo: 'agent-id',
scopes: [
'override.activate:exercise',
'override.activate:sleep',
'override.suggest:*'
],
constraints: {
maxOverrideDuration: 4 * 3600, // 4 hours max
allowedOverrideTypes: ['exercise', 'sleep', 'preMeal'],
requireConfirmation: false,
maxActivationsPerDay: 6,
validTimeWindows: [
{ start: '06:00', end: '22:00' } // Only during waking hours
]
},
grantedAt: '2026-01-01T00:00:00Z',
expiresAt: '2026-12-31T23:59:59Z',
revokedAt: null
};
```
### Grant Validation
```javascript
function validateDelegation(action, agent, grants) {
const applicableGrants = grants.filter(g =>
g.grantedTo === agent.issuerId &&
!g.revokedAt &&
new Date() < new Date(g.expiresAt)
);
for (const grant of applicableGrants) {
if (!scopeMatches(action, grant.scopes)) continue;
if (!withinConstraints(action, grant.constraints)) continue;
if (!withinTimeWindow(grant.constraints.validTimeWindows)) continue;
return {
valid: true,
grantId: grant.grantId,
effectiveAuthority: 'delegated'
};
}
return {
valid: false,
reason: 'no_valid_delegation',
availableGrants: applicableGrants.length
};
}
```
---
## Safety Invariants
### Never Violated
1. **Human always wins** — Human-initiated action cannot be blocked by lower authority
2. **Safety limits respected** — Composed effects never exceed safety limits
3. **Conservative composition** — When in doubt, choose the safer option
4. **Audit everything** — All conflict resolutions are logged
5. **Explicit revocation** — Delegations must be explicitly revoked
### Validated on Every Write
```javascript
function validateSafetyInvariants(proposedState) {
const checks = [
checkTargetRangeValid(proposedState.effectiveTarget),
checkBasalWithinLimits(proposedState.effectiveBasal),
checkIOBWithinLimits(proposedState.currentIOB),
checkNoConflictingOverrides(proposedState.activeOverrides),
checkAuthorityHierarchy(proposedState.recentActions)
];
const violations = checks.filter(c => !c.valid);
if (violations.length > 0) {
throw new SafetyViolationError(violations);
}
return true;
}
```
---
## Configuration
```javascript
const CONFLICT_RESOLUTION_CONFIG = {
// Authority levels
authorityLevels: {
'human-primary': 100,
'human-caregiver': 80,
'agent': 50,
'controller': 30,
'system': 10
},
// Composition strategy
compositionStrategy: 'conservative', // or 'permissive'
// Rate limiting
rateLimits: {
agent: {
overrideActivationsPerHour: 4,
profileSwitchesPerDay: 6
},
controller: {
// Controllers are not rate-limited (they self-regulate)
}
},
// Cooldowns
cooldowns: {
overrideReactivation: 900, // 15 minutes
profileSwitch: 300 // 5 minutes
},
// Confirmation requirements
confirmationRequired: {
agentAfterNActivations: 2,
highRiskActions: ['override.illness', 'profile.switch']
}
};
```
+277
View File
@@ -0,0 +1,277 @@
# Integration Questionnaire for Loop/AAPS/Trio Implementers
**Purpose:** This questionnaire helps determine how well existing AID controllers can support the proposed event-driven control plane architecture. Answers will inform bridge mode implementation and native event adoption roadmaps.
---
## A) Profiles & Overrides Semantics
### A1. Profile Identification
**Question:** Do you have stable identifiers for a profile (beyond just the name)?
| Controller | Current State | Can Emit Content Hash? |
|------------|--------------|----------------------|
| Loop | | |
| Trio | | |
| AAPS | | |
| OpenAPS | | |
**Follow-up:** If not stable IDs, can you emit a content hash of the profile?
---
### A2. Override Templates vs. Activations
**Question:** Can you represent overrides as separate concepts?
- **Template/Definition:** The reusable preset (e.g., "Exercise mode - 140 target, 70% basal")
- **Instance/Activation:** A concrete use of that template
| Controller | Has Templates? | Templates Persisted? | Can Separate? |
|------------|---------------|---------------------|---------------|
| Loop | | | |
| Trio | | | |
| AAPS | | | |
---
### A3. Override Dimensions
**Question:** What override adjustment dimensions exist in your controller?
| Dimension | Loop | Trio | AAPS | OpenAPS |
|-----------|------|------|------|---------|
| Target range (min/max) | | | | |
| Single target value | | | | |
| Sensitivity multiplier | | | | |
| Basal multiplier/percentage | | | | |
| Max basal ceiling | | | | |
| Carb ratio multiplier | | | | |
| Automation aggressiveness | | | | |
| Duration (fixed/indefinite) | | | | |
---
### A4. Multiple Override Resolution
**Question:** How do you resolve multiple concurrent overrides?
- [ ] Only one override can be active at a time
- [ ] Overrides stack/compose with precedence rules
- [ ] Most recent override wins
- [ ] User must explicitly end one before starting another
- [ ] Other: _____________
**Precedence rules (if applicable):**
```
Describe how conflicts are resolved...
```
---
## B) Policy Composition
### B5. Effective Parameters Computation
**Question:** Do you compute an explicit "effective therapy settings right now" object internally?
| Controller | Has Explicit Object? | Fields Included |
|------------|---------------------|-----------------|
| Loop | | |
| Trio | | |
| AAPS | | |
---
### B6. Composition Emission
**Question:** Can you emit a composition snapshot containing:
| Field | Loop | Trio | AAPS | Notes |
|-------|------|------|------|-------|
| Profile hash/ID | | | | |
| Active override IDs | | | | |
| Effective target range | | | | |
| Effective ISF (current) | | | | |
| Effective CR (current) | | | | |
| Effective basal (current) | | | | |
| Safety limits in force | | | | |
| Controller version/build | | | | |
**If "yes" to most fields:** Nightscout's digital twin becomes reliable without simulating the algorithm.
---
## C) Delivery Fidelity
### C7. Delivery State Distinction
**Question:** Can you distinguish clearly between:
| State | Loop | Trio | AAPS | Where Reported |
|-------|------|------|------|----------------|
| **Suggested/recommended** (algorithm output) | | | | |
| **Requested** (command sent to pump) | | | | |
| **Confirmed/enacted** (pump acknowledged) | | | | |
---
### C8. Pump Response Codes
**Question:** Do you have pump ACK/NAK/error codes that can be surfaced?
| Controller | Has Error Codes | Examples | Can Expose? |
|------------|----------------|----------|-------------|
| Loop | | | |
| Trio | | | |
| AAPS | | | |
---
### C9. Failure Reason Distinction
**Question:** How do you represent different failure modes?
| Failure Type | Loop | Trio | AAPS |
|-------------|------|------|------|
| Capped by max basal limit | | | |
| Capped by max IOB | | | |
| Communication failure with pump | | | |
| Pump busy/unavailable | | | |
| User canceled | | | |
| Pump error (occlusion, etc.) | | | |
**Current representation format:** (e.g., error codes, status strings, flags)
```
...
```
---
## D) Timing and Ordering
### D10. Monotonic Ordering
**Question:** Can you provide monotonic ordering per controller?
| Controller | Has Sequence Numbers | Clock Guarantees | Ordering Method |
|------------|---------------------|------------------|-----------------|
| Loop | | | |
| Trio | | | |
| AAPS | | | |
**If no sequence numbers:** What clock/timing guarantees exist?
---
### D11. Offline Batching
**Question:** How do you handle offline operation and delayed uploads?
| Scenario | Loop | Trio | AAPS |
|----------|------|------|------|
| Queue events when offline | | | |
| Batch upload on reconnect | | | |
| Preserve ordering in batch | | | |
| Mark events as "delayed" | | | |
| Dedup on retry | | | |
---
## E) Minimal Commitment for Phase 1
### E12. Smallest Native Event Set
**Question:** What is the smallest set of native events you're willing to emit first?
**Proposed minimal set for Phase 1:**
| Event | Priority | Loop | Trio | AAPS |
|-------|----------|------|------|------|
| `override.instance.activated` | High | | | |
| `override.instance.ended` | High | | | |
| `policy.composition.computed` | High | | | |
| `delivery.observed` (summary) | Medium | | | |
| `profile.selection.changed` | Medium | | | |
| `capability.snapshot.updated` | Low | | | |
**Your minimal commitment:**
```
List the events you can commit to emitting...
```
---
## F) Additional Capabilities
### F13. Remote Commands
**Question:** Can your controller accept commands from Nightscout?
| Command Type | Loop | Trio | AAPS | Security Model |
|-------------|------|------|------|----------------|
| Activate override | | | | |
| End override | | | | |
| Switch profile | | | | |
| Set temp target | | | | |
| Bolus (caregiving) | | | | |
---
### F14. Controller Registration
**Question:** Can you emit controller registration events?
| Field | Loop | Trio | AAPS |
|-------|------|------|------|
| Unique instance ID | | | |
| Device info | | | |
| Pump binding info | | | |
| CGM binding info | | | |
| Version/build | | | |
---
## G) Migration Path
### G15. Adoption Timeline
**Question:** What's your estimated timeline for native event emission?
| Phase | Loop | Trio | AAPS |
|-------|------|------|------|
| Can test in dev builds | | | |
| Can ship in beta | | | |
| Can ship in stable | | | |
| Full native (no devicestatus) | | | |
---
### G16. Bridge Mode Feedback
**Question:** What concerns do you have about Nightscout synthesizing events from your devicestatus uploads?
- [ ] Accuracy of override state extraction
- [ ] Accuracy of delivery extraction
- [ ] Missing fields in current devicestatus
- [ ] Timing/ordering issues
- [ ] Duplicate events
- [ ] Other: _____________
---
## How to Submit
Please fill out this questionnaire and submit via:
1. **GitHub Issue** on the Nightscout cgm-remote-monitor repository with label `agentic-control-plane`
2. **Pull Request** adding your controller's responses to this file
3. **Discord** in the #development channel
---
## Contact
Questions about this questionnaire:
- Nightscout Discord: [link]
- GitHub Discussions: [link]
---
## Responses
### Loop
*Status: Pending*
### Trio
*Status: Pending*
### AAPS
*Status: Pending*
### OpenAPS
*Status: Pending*
@@ -0,0 +1,181 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://nightscout.github.io/schemas/capability-snapshot.schema.json",
"title": "CapabilitySnapshot",
"description": "Point-in-time snapshot of what the controller can actually do right now",
"type": "object",
"required": ["snapshotId", "controllerInstanceId", "snapshotAt"],
"properties": {
"snapshotId": {
"type": "string",
"format": "uuid",
"description": "Unique identifier for this snapshot"
},
"controllerInstanceId": {
"type": "string",
"description": "Reference to ControllerInstanceRegistration"
},
"connectivity": {
"type": "object",
"description": "Current connectivity status",
"properties": {
"pumpConnected": {
"type": "boolean",
"description": "Is pump currently connected"
},
"pumpLastContact": {
"type": "string",
"format": "date-time",
"description": "Last successful communication with pump"
},
"pumpRSSI": {
"type": "integer",
"description": "Pump radio signal strength"
},
"cgmConnected": {
"type": "boolean",
"description": "Is CGM currently connected"
},
"cgmLastReading": {
"type": "string",
"format": "date-time",
"description": "Timestamp of last CGM reading"
},
"internetConnected": {
"type": "boolean",
"description": "Is device online"
}
}
},
"automationState": {
"type": "object",
"description": "Current automation status",
"properties": {
"closedLoopEnabled": {
"type": "boolean",
"description": "Is closed-loop automation active"
},
"suspended": {
"type": "boolean",
"description": "Is insulin delivery suspended"
},
"suspendReason": {
"type": "string",
"description": "Why suspended (if applicable)"
},
"lastLoopTime": {
"type": "string",
"format": "date-time",
"description": "When loop algorithm last ran"
},
"lastEnactTime": {
"type": "string",
"format": "date-time",
"description": "When last delivery was enacted"
}
}
},
"effectiveLimits": {
"type": "object",
"description": "Current safety limits in effect",
"properties": {
"maxBasal": {
"type": "number",
"description": "Maximum basal rate allowed (U/hr)"
},
"maxBolus": {
"type": "number",
"description": "Maximum bolus allowed (U)"
},
"maxIOB": {
"type": "number",
"description": "Maximum IOB allowed (U)"
},
"minBG": {
"type": "number",
"description": "Minimum target BG"
}
}
},
"health": {
"type": "object",
"description": "Device health indicators",
"properties": {
"reservoirUnits": {
"type": "number",
"description": "Remaining insulin units in reservoir"
},
"reservoirLevel": {
"type": "string",
"enum": ["full", "adequate", "low", "critical"],
"description": "Reservoir level category"
},
"batteryPercent": {
"type": "integer",
"minimum": 0,
"maximum": 100,
"description": "Pump battery percentage"
},
"phoneBatteryPercent": {
"type": "integer",
"minimum": 0,
"maximum": 100,
"description": "Controller device battery"
},
"podAge": {
"type": "integer",
"description": "Hours since pod was activated (for pod pumps)"
},
"podExpires": {
"type": "string",
"format": "date-time",
"description": "When pod expires"
},
"cgmCalibrationStatus": {
"type": "string",
"enum": ["ok", "needed", "overdue", "na"],
"description": "CGM calibration status"
},
"sensorAge": {
"type": "integer",
"description": "Hours since sensor was started"
},
"timeSyncHealth": {
"type": "string",
"enum": ["good", "drift", "unknown"],
"description": "Clock synchronization status"
}
}
},
"degradedCapabilities": {
"type": "array",
"description": "Capabilities that are currently degraded or unavailable",
"items": {
"type": "object",
"properties": {
"capability": {
"type": "string",
"description": "Which capability is affected"
},
"reason": {
"type": "string",
"description": "Why it's degraded"
},
"since": {
"type": "string",
"format": "date-time"
}
}
}
},
"snapshotAt": {
"type": "string",
"format": "date-time",
"description": "When this snapshot was taken"
},
"validFor": {
"type": "integer",
"description": "How long this snapshot is valid (seconds)"
}
}
}
@@ -0,0 +1,118 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://nightscout.github.io/schemas/controller-instance-registration.schema.json",
"title": "ControllerInstanceRegistration",
"description": "Registration of a specific controller instance (this phone/device running right now)",
"type": "object",
"required": ["instanceId", "kindId", "version", "device", "registeredAt"],
"properties": {
"instanceId": {
"type": "string",
"format": "uuid",
"description": "Unique identifier for this controller instance"
},
"kindId": {
"type": "string",
"description": "Reference to ControllerKindDefinition"
},
"version": {
"type": "string",
"description": "Specific version/build of the controller app"
},
"device": {
"type": "object",
"required": ["deviceId", "platform"],
"description": "Device information",
"properties": {
"deviceId": {
"type": "string",
"description": "Unique device identifier"
},
"platform": {
"type": "string",
"enum": ["ios", "android", "linux", "macos", "windows"],
"description": "Operating system platform"
},
"model": {
"type": "string",
"description": "Device model (iPhone 14, Pixel 7, etc.)"
},
"osVersion": {
"type": "string",
"description": "OS version"
},
"appBuild": {
"type": "string",
"description": "App build number"
}
}
},
"pumpBinding": {
"type": "object",
"description": "Currently bound pump",
"properties": {
"pumpKind": {
"type": "string",
"description": "Type of pump (omnipod, omnipod-dash, medtronic-x23, etc.)"
},
"pumpSerial": {
"type": "string",
"description": "Pump serial number (may be partial/hashed)"
},
"pumpFirmware": {
"type": "string",
"description": "Pump firmware version"
},
"connectedSince": {
"type": "string",
"format": "date-time",
"description": "When pump was connected"
}
}
},
"cgmBinding": {
"type": "object",
"description": "Currently bound CGM",
"properties": {
"cgmKind": {
"type": "string",
"description": "Type of CGM (dexcom-g6, dexcom-g7, libre-3, etc.)"
},
"cgmId": {
"type": "string",
"description": "CGM identifier (transmitter ID, sensor ID)"
},
"cgmFirmware": {
"type": "string"
},
"sensorStarted": {
"type": "string",
"format": "date-time"
},
"sensorExpires": {
"type": "string",
"format": "date-time"
}
}
},
"registeredAt": {
"type": "string",
"format": "date-time",
"description": "When this instance was first registered"
},
"lastSeenAt": {
"type": "string",
"format": "date-time",
"description": "When this instance last communicated"
},
"status": {
"type": "string",
"enum": ["active", "inactive", "deregistered"],
"description": "Current status of this registration"
},
"deregisteredAt": {
"type": "string",
"format": "date-time"
}
}
}
@@ -0,0 +1,117 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://nightscout.github.io/schemas/controller-kind-definition.schema.json",
"title": "ControllerKindDefinition",
"description": "Declared capabilities and schema for a type of AID controller (Loop, Trio, AAPS, etc.)",
"type": "object",
"required": ["kindId", "name", "version"],
"properties": {
"kindId": {
"type": "string",
"description": "Unique identifier for this controller type",
"examples": ["loop", "trio", "aaps", "openaps", "tidepool-loop"]
},
"name": {
"type": "string",
"description": "Human-readable name"
},
"version": {
"type": "string",
"description": "Version of this controller definition"
},
"vendor": {
"type": "string",
"description": "Organization or project responsible"
},
"supportedFeatures": {
"type": "object",
"description": "Feature flags for this controller type",
"properties": {
"tempBasal": {
"type": "boolean",
"description": "Can set temporary basal rates"
},
"microBolus": {
"type": "boolean",
"description": "Uses micro-bolus (SMB) strategy"
},
"suspend": {
"type": "boolean",
"description": "Can suspend insulin delivery"
},
"overrides": {
"type": "boolean",
"description": "Supports override presets"
},
"autoSens": {
"type": "boolean",
"description": "Has autosensitivity detection"
},
"dynamicISF": {
"type": "boolean",
"description": "Supports dynamic ISF adjustments"
},
"dynamicCR": {
"type": "boolean",
"description": "Supports dynamic carb ratio adjustments"
},
"smbWithCOB": {
"type": "boolean",
"description": "Allows SMB when carbs on board"
},
"uam": {
"type": "boolean",
"description": "Supports unannounced meals detection"
},
"profileSwitching": {
"type": "boolean",
"description": "Supports switching between profiles"
},
"remoteCommands": {
"type": "boolean",
"description": "Can accept remote commands"
}
}
},
"supportedPumps": {
"type": "array",
"items": { "type": "string" },
"description": "List of compatible pump types"
},
"supportedCGMs": {
"type": "array",
"items": { "type": "string" },
"description": "List of compatible CGM types"
},
"algorithmType": {
"type": "string",
"enum": ["oref0", "oref1", "dosing-decision", "custom"],
"description": "Core algorithm family"
},
"eventCapabilities": {
"type": "object",
"description": "What events this controller can emit",
"properties": {
"canEmitNativeEvents": {
"type": "boolean",
"description": "Whether it can emit canonical events directly"
},
"minimalEventSet": {
"type": "array",
"items": { "type": "string" },
"description": "Minimum set of event types it can emit"
},
"deviceStatusFields": {
"type": "array",
"items": { "type": "string" },
"description": "Fields available in devicestatus uploads"
}
}
},
"documentation": {
"type": "string",
"format": "uri",
"description": "Link to controller documentation"
}
}
}
@@ -0,0 +1,135 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://nightscout.github.io/schemas/delivery-observation.schema.json",
"title": "DeliveryObservation",
"description": "Observed/confirmed insulin delivery from any source (pump, pen, injection)",
"type": "object",
"required": ["observationId", "observationType", "source", "observed", "reportedBy", "observedAt"],
"properties": {
"observationId": {
"type": "string",
"format": "uuid",
"description": "Unique identifier for this observation"
},
"observationType": {
"type": "string",
"enum": ["basal", "tempBasal", "bolus", "microBolus", "suspend", "injection", "pen", "inhaler"],
"description": "Type of delivery observed"
},
"source": {
"type": "object",
"required": ["sourceType"],
"description": "Where this delivery came from",
"properties": {
"sourceType": {
"type": "string",
"enum": ["pump", "manual", "pen", "inhaler", "estimated"],
"description": "Category of delivery source"
},
"sourceId": {
"type": "string",
"description": "Device identifier if from pump"
},
"sourceKind": {
"type": "string",
"description": "Specific device type (omnipod, medtronic, tandem, novopen, etc.)"
}
}
},
"observed": {
"type": "object",
"description": "What was actually delivered",
"properties": {
"rate": {
"type": "number",
"minimum": 0,
"description": "Observed rate in U/hr (for basal)"
},
"units": {
"type": "number",
"minimum": 0,
"description": "Observed units delivered"
},
"duration": {
"type": "integer",
"minimum": 0,
"description": "Duration in seconds (for temp basal)"
},
"startTime": {
"type": "string",
"format": "date-time",
"description": "When delivery started"
},
"endTime": {
"type": "string",
"format": "date-time",
"description": "When delivery ended (if completed)"
}
}
},
"confidence": {
"type": "string",
"enum": ["confirmed", "inferred", "reported", "estimated"],
"description": "Confidence level of this observation"
},
"reportedBy": {
"type": "object",
"required": ["issuerType", "issuerId"],
"properties": {
"issuerType": {
"type": "string",
"enum": ["human", "controller", "agent", "caregiver", "pump"]
},
"issuerId": {
"type": "string"
}
}
},
"observedAt": {
"type": "string",
"format": "date-time",
"description": "When this observation was recorded"
},
"pumpResponse": {
"type": "object",
"description": "Response from pump if applicable",
"properties": {
"acked": {
"type": "boolean",
"description": "Whether pump acknowledged the command"
},
"errorCode": {
"type": "string",
"description": "Error code if failed"
},
"errorMessage": {
"type": "string",
"description": "Human-readable error message"
},
"rawResponse": {
"type": "object",
"description": "Raw pump response for debugging"
}
}
},
"relatedRequestId": {
"type": ["string", "null"],
"format": "uuid",
"description": "DeliveryRequest this observation corresponds to, if any"
},
"annotations": {
"type": "object",
"description": "Additional context",
"properties": {
"notes": {
"type": "string",
"description": "Human-provided notes (e.g., 'correction for high')"
},
"mealContext": {
"type": "string",
"enum": ["breakfast", "lunch", "dinner", "snack", "correction", "other"]
}
}
}
}
}
@@ -0,0 +1,100 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://nightscout.github.io/schemas/delivery-request.schema.json",
"title": "DeliveryRequest",
"description": "Intent to deliver insulin - the command sent to the pump",
"type": "object",
"required": ["requestId", "requestType", "requestedBy", "requestedAt"],
"properties": {
"requestId": {
"type": "string",
"format": "uuid",
"description": "Unique identifier for this delivery request"
},
"requestType": {
"type": "string",
"enum": ["tempBasal", "scheduledBasal", "bolus", "microBolus", "suspend", "resume", "cancelTemp"],
"description": "Type of delivery action requested"
},
"parameters": {
"type": "object",
"description": "Request parameters (varies by requestType)",
"properties": {
"rate": {
"type": "number",
"minimum": 0,
"description": "Rate in U/hr for basal requests"
},
"units": {
"type": "number",
"minimum": 0,
"description": "Units for bolus requests"
},
"duration": {
"type": "integer",
"minimum": 0,
"description": "Duration in seconds for temp basal"
},
"bolusType": {
"type": "string",
"enum": ["normal", "square", "dual", "micro"],
"description": "Type of bolus if applicable"
}
}
},
"requestedBy": {
"type": "object",
"required": ["issuerType", "issuerId"],
"properties": {
"issuerType": {
"type": "string",
"enum": ["human", "controller", "agent", "caregiver"]
},
"issuerId": {
"type": "string"
}
}
},
"basedOn": {
"type": "object",
"description": "What informed this request",
"properties": {
"policyCompositionId": {
"type": "string",
"description": "PolicyComposition that was in force"
},
"algorithmSuggestion": {
"type": "object",
"description": "Algorithm output that led to this request",
"properties": {
"suggestedAt": { "type": "string", "format": "date-time" },
"reason": { "type": "string" },
"predictedBG": {
"type": "array",
"items": { "type": "number" }
},
"iob": { "type": "number" },
"cob": { "type": "number" },
"eventualBG": { "type": "number" }
}
}
}
},
"requestedAt": {
"type": "string",
"format": "date-time",
"description": "When this request was created"
},
"expiresAt": {
"type": "string",
"format": "date-time",
"description": "Request is stale/invalid after this time"
},
"priority": {
"type": "string",
"enum": ["normal", "high", "urgent"],
"default": "normal",
"description": "Request priority for queuing"
}
}
}
@@ -0,0 +1,107 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://nightscout.github.io/schemas/event-envelope.schema.json",
"title": "EventEnvelope",
"description": "Wrapper for all control plane events, providing consistent ordering, audit, and replay capabilities",
"type": "object",
"required": ["eventId", "eventType", "issuer", "timestamp", "payload"],
"properties": {
"eventId": {
"type": "string",
"format": "uuid",
"description": "Stable UUID for this event"
},
"eventType": {
"type": "string",
"description": "Dot-notation event type",
"enum": [
"profile.definition.created",
"profile.definition.updated",
"profile.selection.changed",
"override.definition.created",
"override.definition.updated",
"override.instance.activated",
"override.instance.ended",
"override.instance.canceled",
"override.instance.superseded",
"policy.composition.computed",
"delivery.requested",
"delivery.observed",
"delivery.reconciled",
"controller.registered",
"controller.deregistered",
"capability.snapshot.updated"
]
},
"cursor": {
"type": "integer",
"minimum": 0,
"description": "Server-assigned monotonic global ordering"
},
"issuer": {
"type": "string",
"description": "Identifier for the entity that issued this event (controller, user, agent)"
},
"issuerSeq": {
"type": "integer",
"minimum": 0,
"description": "Monotonic sequence number per issuer for ordering"
},
"idempotencyKey": {
"type": "string",
"description": "Client-provided key for retry deduplication"
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp when event was created"
},
"refs": {
"type": "array",
"description": "References to related objects",
"items": {
"type": "object",
"required": ["refType", "refId"],
"properties": {
"refType": {
"type": "string",
"description": "Type of referenced object",
"enum": [
"profileDefinition",
"profileSelection",
"overrideDefinition",
"overrideInstance",
"policyComposition",
"deliveryRequest",
"deliveryObservation",
"controllerInstance",
"capabilitySnapshot"
]
},
"refId": {
"type": "string",
"description": "ID or content hash of referenced object"
}
}
}
},
"payload": {
"type": "object",
"description": "The actual event data (schema depends on eventType)"
},
"metadata": {
"type": "object",
"description": "Optional extensible metadata",
"properties": {
"bridgeSource": {
"type": "string",
"description": "If synthesized from legacy data, the source collection"
},
"bridgeSourceId": {
"type": "string",
"description": "If synthesized, the source document ID"
}
}
}
}
}
@@ -0,0 +1,116 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://nightscout.github.io/schemas/override-definition.schema.json",
"title": "OverrideDefinition",
"description": "User-authored reusable template for therapy overrides (exercise, sleep, illness, etc.)",
"type": "object",
"required": ["definitionId", "overrideType", "title", "createdBy", "createdAt"],
"properties": {
"definitionId": {
"type": "string",
"description": "Unique identifier for this override definition"
},
"overrideType": {
"type": "string",
"enum": ["exercise", "sleep", "preMeal", "illness", "highActivity", "hormones", "stress", "travel", "custom"],
"description": "Category of override"
},
"title": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Human-readable name for this override"
},
"description": {
"type": "string",
"maxLength": 500,
"description": "Optional longer description of when to use this override"
},
"defaultDuration": {
"type": ["integer", "null"],
"minimum": 0,
"description": "Default duration in seconds; null means indefinite until manually ended"
},
"effects": {
"type": "object",
"description": "Therapy adjustments when this override is active",
"properties": {
"targetRange": {
"type": "object",
"description": "Absolute target range override",
"properties": {
"low": {
"type": "number",
"minimum": 60,
"description": "Lower target bound in configured units"
},
"high": {
"type": "number",
"minimum": 60,
"description": "Upper target bound in configured units"
}
}
},
"targetDelta": {
"type": "number",
"description": "Relative adjustment to existing target (positive = raise target)"
},
"basalMultiplier": {
"type": "number",
"minimum": 0,
"maximum": 5,
"description": "Multiplier for basal rate (1.0 = no change, 0.5 = 50%, 1.5 = 150%)"
},
"maxBasalCeiling": {
"type": "number",
"minimum": 0,
"description": "Maximum basal rate cap in U/hr"
},
"sensitivityMultiplier": {
"type": "number",
"minimum": 0.1,
"maximum": 5,
"description": "Multiplier for ISF (>1 = more sensitive, <1 = more resistant)"
},
"carbRatioMultiplier": {
"type": "number",
"minimum": 0.1,
"maximum": 5,
"description": "Multiplier for carb ratio"
},
"automationAggressiveness": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Scaling factor for automation aggressiveness if controller supports"
}
}
},
"createdBy": {
"type": "object",
"required": ["issuerType", "issuerId"],
"properties": {
"issuerType": {
"type": "string",
"enum": ["human", "controller", "agent", "caregiver"]
},
"issuerId": {
"type": "string"
}
}
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
},
"isArchived": {
"type": "boolean",
"default": false,
"description": "Soft delete - archived definitions cannot be instantiated"
}
}
}
@@ -0,0 +1,127 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://nightscout.github.io/schemas/override-instance.schema.json",
"title": "OverrideInstance",
"description": "A concrete activation of an override, representing runtime intent to modify therapy parameters",
"type": "object",
"required": ["instanceId", "start", "status", "requestedBy"],
"properties": {
"instanceId": {
"type": "string",
"format": "uuid",
"description": "Unique identifier for this override activation"
},
"definitionId": {
"type": ["string", "null"],
"description": "Reference to OverrideDefinition if using a template; null for ad-hoc overrides"
},
"start": {
"type": "string",
"format": "date-time",
"description": "When this override becomes active"
},
"duration": {
"type": ["integer", "null"],
"minimum": 0,
"description": "Duration in seconds; null means indefinite"
},
"end": {
"type": ["string", "null"],
"format": "date-time",
"description": "When this override ends (computed from start+duration or explicit)"
},
"effectiveEffects": {
"type": "object",
"description": "Resolved effects for this instance (may differ from definition due to overrides)",
"properties": {
"targetRange": {
"type": "object",
"properties": {
"low": { "type": "number" },
"high": { "type": "number" }
}
},
"basalMultiplier": { "type": "number" },
"sensitivityMultiplier": { "type": "number" },
"carbRatioMultiplier": { "type": "number" },
"maxBasalCeiling": { "type": "number" }
}
},
"requestedBy": {
"type": "object",
"required": ["issuerType", "issuerId", "authority"],
"properties": {
"issuerType": {
"type": "string",
"enum": ["human", "controller", "agent", "caregiver"]
},
"issuerId": {
"type": "string"
},
"authority": {
"type": "string",
"enum": ["primary", "delegated", "automated"],
"description": "Authority level of the requester"
}
}
},
"status": {
"type": "string",
"enum": ["pending", "active", "ended", "canceled", "superseded"],
"description": "Current status of this override instance"
},
"supersededBy": {
"type": ["string", "null"],
"format": "uuid",
"description": "If superseded, the instanceId of the override that replaced this one"
},
"endedBy": {
"type": "object",
"description": "Who ended this override (if manually ended)",
"properties": {
"issuerType": {
"type": "string",
"enum": ["human", "controller", "agent", "caregiver"]
},
"issuerId": { "type": "string" },
"endedAt": {
"type": "string",
"format": "date-time"
}
}
},
"reason": {
"type": "string",
"maxLength": 500,
"description": "Why this override was activated"
},
"annotations": {
"type": "object",
"description": "Extensible metadata",
"properties": {
"externalContext": {
"type": "object",
"description": "External data that informed this override",
"properties": {
"hormoneCycleDay": { "type": "integer" },
"activityLevel": { "type": "string" },
"geolocation": { "type": "string" },
"stressIndicator": { "type": "number" }
}
},
"agentReasoning": {
"type": "string",
"description": "If agent-initiated, explanation of reasoning"
}
}
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
}
}
@@ -0,0 +1,146 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://nightscout.github.io/schemas/policy-composition.schema.json",
"title": "PolicyComposition",
"description": "Materialized view of effective therapy parameters computed from active profile and overrides",
"type": "object",
"required": ["compositionId", "references", "effectiveParameters", "computedBy", "validFrom"],
"properties": {
"compositionId": {
"type": "string",
"format": "uuid",
"description": "Unique identifier for this composition snapshot"
},
"references": {
"type": "object",
"required": ["profileId", "profileHash"],
"description": "References to source objects used in this composition",
"properties": {
"profileId": {
"type": "string",
"description": "Active ProfileDefinition ID"
},
"profileHash": {
"type": "string",
"pattern": "^[a-f0-9]{64}$",
"description": "Content hash of active profile for verification"
},
"activeOverrideInstanceIds": {
"type": "array",
"items": { "type": "string" },
"description": "Currently active OverrideInstance IDs"
},
"capabilitySnapshotId": {
"type": ["string", "null"],
"description": "Reference to CapabilitySnapshot if available"
}
}
},
"effectiveParameters": {
"type": "object",
"description": "Computed therapy parameters currently in force",
"properties": {
"targetRange": {
"type": "object",
"required": ["low", "high"],
"properties": {
"low": {
"type": "number",
"description": "Effective lower target bound"
},
"high": {
"type": "number",
"description": "Effective upper target bound"
}
}
},
"effectiveISF": {
"type": "number",
"description": "Effective insulin sensitivity factor for current time"
},
"effectiveCR": {
"type": "number",
"description": "Effective carb ratio for current time"
},
"effectiveBasal": {
"type": "number",
"description": "Effective basal rate for current time (after multipliers)"
},
"scheduledBasal": {
"type": "number",
"description": "Scheduled basal rate from profile (before multipliers)"
},
"basalMultiplier": {
"type": "number",
"description": "Aggregate basal multiplier from overrides"
},
"sensitivityMultiplier": {
"type": "number",
"description": "Aggregate sensitivity multiplier from overrides"
},
"maxBasalAllowed": {
"type": "number",
"description": "Maximum basal rate allowed by constraints"
},
"maxBolusAllowed": {
"type": "number",
"description": "Maximum bolus allowed by constraints"
},
"maxIOB": {
"type": "number",
"description": "Maximum IOB allowed by constraints"
},
"automationEnabled": {
"type": "boolean",
"description": "Whether closed-loop automation is active"
},
"automationMode": {
"type": "string",
"enum": ["closedLoop", "openLoop", "suspended", "manual"],
"description": "Current automation mode"
}
}
},
"computedBy": {
"type": "object",
"required": ["computedAt"],
"properties": {
"controllerKind": {
"type": "string",
"description": "Controller type that computed this (loop, trio, aaps, nightscout)"
},
"controllerVersion": {
"type": "string",
"description": "Controller version"
},
"controllerInstanceId": {
"type": "string",
"description": "Specific controller instance ID"
},
"computedAt": {
"type": "string",
"format": "date-time",
"description": "When this composition was computed"
}
}
},
"validFrom": {
"type": "string",
"format": "date-time",
"description": "When this composition became valid"
},
"validTo": {
"type": ["string", "null"],
"format": "date-time",
"description": "When this composition was superseded; null if current"
},
"cursor": {
"type": "integer",
"description": "Global ordering cursor for event stream"
},
"previousCompositionId": {
"type": ["string", "null"],
"description": "Previous composition for audit chain"
}
}
}
@@ -0,0 +1,176 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://nightscout.github.io/schemas/profile-definition.schema.json",
"title": "ProfileDefinition",
"description": "User-authored, versioned profile configuration containing basal rates, ISF, CR, and targets",
"type": "object",
"required": ["profileId", "contentHash", "title", "timezone", "units", "schedules", "createdBy", "createdAt"],
"properties": {
"profileId": {
"type": "string",
"description": "Stable unique identifier for this profile"
},
"contentHash": {
"type": "string",
"pattern": "^[a-f0-9]{64}$",
"description": "SHA-256 hash of canonicalized profile content for deduplication and verification"
},
"title": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Human-readable profile name"
},
"timezone": {
"type": "string",
"description": "IANA timezone identifier (e.g., 'America/New_York')"
},
"units": {
"type": "string",
"enum": ["mg/dL", "mmol/L"],
"description": "Blood glucose units"
},
"schedules": {
"type": "object",
"required": ["basal", "isf", "cr", "target"],
"properties": {
"basal": {
"type": "array",
"minItems": 1,
"description": "Time-based basal rate schedule",
"items": {
"type": "object",
"required": ["time", "rate"],
"properties": {
"time": {
"type": "string",
"pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$",
"description": "Start time in HH:MM format"
},
"rate": {
"type": "number",
"minimum": 0,
"maximum": 35,
"description": "Basal rate in U/hr"
}
}
}
},
"isf": {
"type": "array",
"minItems": 1,
"description": "Insulin sensitivity factor schedule",
"items": {
"type": "object",
"required": ["time", "value"],
"properties": {
"time": {
"type": "string",
"pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$"
},
"value": {
"type": "number",
"minimum": 1,
"description": "ISF in configured units per 1U insulin"
}
}
}
},
"cr": {
"type": "array",
"minItems": 1,
"description": "Carb ratio schedule",
"items": {
"type": "object",
"required": ["time", "value"],
"properties": {
"time": {
"type": "string",
"pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$"
},
"value": {
"type": "number",
"minimum": 1,
"description": "Grams of carbs per 1U insulin"
}
}
}
},
"target": {
"type": "array",
"minItems": 1,
"description": "Target glucose range schedule",
"items": {
"type": "object",
"required": ["time", "low", "high"],
"properties": {
"time": {
"type": "string",
"pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$"
},
"low": {
"type": "number",
"minimum": 60,
"description": "Lower target bound"
},
"high": {
"type": "number",
"minimum": 60,
"description": "Upper target bound"
}
}
}
}
}
},
"insulinModel": {
"type": "object",
"description": "Insulin action curve parameters",
"properties": {
"type": {
"type": "string",
"enum": ["rapid", "fiasp", "lyumjev", "afrezza", "custom"],
"description": "Insulin model type"
},
"dia": {
"type": "number",
"minimum": 2,
"maximum": 10,
"description": "Duration of insulin action in hours"
},
"peakTime": {
"type": "number",
"minimum": 15,
"maximum": 180,
"description": "Time to peak action in minutes"
}
}
},
"createdBy": {
"$ref": "#/$defs/issuerRef"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"legacyProfileName": {
"type": "string",
"description": "For backward compatibility mapping to legacy profile name field"
}
},
"$defs": {
"issuerRef": {
"type": "object",
"required": ["issuerType", "issuerId"],
"properties": {
"issuerType": {
"type": "string",
"enum": ["human", "controller", "agent", "caregiver"]
},
"issuerId": {
"type": "string"
}
}
}
}
}
@@ -0,0 +1,57 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://nightscout.github.io/schemas/profile-selection.schema.json",
"title": "ProfileSelection",
"description": "Runtime event indicating which profile is intended to be active",
"type": "object",
"required": ["selectionId", "selectedProfileId", "effectiveAt", "selectedBy"],
"properties": {
"selectionId": {
"type": "string",
"format": "uuid",
"description": "Unique identifier for this selection event"
},
"selectedProfileId": {
"type": "string",
"description": "ID of the ProfileDefinition being activated"
},
"selectedProfileHash": {
"type": "string",
"pattern": "^[a-f0-9]{64}$",
"description": "Content hash of the selected profile for verification"
},
"effectiveAt": {
"type": "string",
"format": "date-time",
"description": "When this profile selection takes effect"
},
"selectedBy": {
"type": "object",
"required": ["issuerType", "issuerId", "authority"],
"properties": {
"issuerType": {
"type": "string",
"enum": ["human", "controller", "agent", "caregiver"]
},
"issuerId": {
"type": "string"
},
"authority": {
"type": "string",
"enum": ["primary", "delegated", "automated"],
"description": "Authority level of the selector"
}
}
},
"reason": {
"type": "string",
"maxLength": 500,
"description": "Optional annotation explaining why this profile was selected"
},
"previousSelectionId": {
"type": "string",
"format": "uuid",
"description": "ID of the selection this replaces, for audit chain"
}
}
}
@@ -0,0 +1,117 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://nightscout.github.io/schemas/reconciliation.schema.json",
"title": "Reconciliation",
"description": "Matching of DeliveryRequest to DeliveryObservation - intent vs reality",
"type": "object",
"required": ["reconciliationId", "outcome", "reconciledAt"],
"properties": {
"reconciliationId": {
"type": "string",
"format": "uuid",
"description": "Unique identifier for this reconciliation"
},
"requestId": {
"type": ["string", "null"],
"format": "uuid",
"description": "The DeliveryRequest being reconciled (null for spontaneous observations)"
},
"observationId": {
"type": ["string", "null"],
"format": "uuid",
"description": "The DeliveryObservation (null for unobserved requests)"
},
"outcome": {
"type": "string",
"enum": ["matched", "partial", "blocked", "failed", "expired", "unknown", "spontaneous"],
"description": "Result of reconciliation"
},
"discrepancy": {
"type": "object",
"description": "Details of any mismatch between request and observation",
"properties": {
"requestedUnits": {
"type": "number",
"description": "What was requested"
},
"deliveredUnits": {
"type": "number",
"description": "What was actually delivered"
},
"delta": {
"type": "number",
"description": "Difference (delivered - requested)"
},
"requestedRate": {
"type": "number",
"description": "Requested rate for temp basal"
},
"deliveredRate": {
"type": "number",
"description": "Actual rate delivered"
},
"requestedDuration": {
"type": "integer",
"description": "Requested duration in seconds"
},
"actualDuration": {
"type": "integer",
"description": "Actual duration in seconds"
},
"reason": {
"type": "string",
"enum": [
"capped_by_max_basal",
"capped_by_max_iob",
"capped_by_max_bolus",
"comm_failure",
"pump_error",
"user_canceled",
"pump_busy",
"reservoir_low",
"battery_low",
"occlusion",
"expired",
"unknown"
],
"description": "Reason for discrepancy"
},
"reasonDetails": {
"type": "string",
"description": "Additional details about the discrepancy"
}
}
},
"reconciledAt": {
"type": "string",
"format": "date-time",
"description": "When this reconciliation was performed"
},
"reconciledBy": {
"type": "object",
"properties": {
"issuerType": {
"type": "string",
"enum": ["controller", "agent", "system"]
},
"issuerId": {
"type": "string"
}
}
},
"latency": {
"type": "object",
"description": "Timing metrics",
"properties": {
"requestToObservationMs": {
"type": "integer",
"description": "Time from request to observation in milliseconds"
},
"requestToReconciliationMs": {
"type": "integer",
"description": "Time from request to reconciliation in milliseconds"
}
}
}
}
}
+38
View File
@@ -145,7 +145,45 @@ Modern REST API with OpenAPI 3.0 spec.
- `lib/api3/doc/alarmsockets.md` - Alarm socket
- `lib/api3/doc/tutorial.md` - API tutorial
## Agentic Control Plane Proposal (RFC)
A proposal for extending Nightscout with a clean separation between control plane (policy, configuration, intent) and data plane (observations, telemetry, delivery) to enable AI agent collaboration with AID systems.
### Proposal Documentation
| Document | Description |
|----------|-------------|
| `docs/proposals/agent-control-plane-rfc.md` | Main RFC document with full architecture |
| `docs/proposals/integration-questionnaire.md` | Questions for Loop/AAPS/Trio implementers |
| `docs/proposals/bridge-rules.md` | Legacy devicestatus → event synthesis rules |
| `docs/proposals/conflict-resolution.md` | Multi-writer semantics and authority model |
### JSON Schemas (draft-2020-12)
Located in `docs/proposals/schemas/`:
| Schema | Purpose |
|--------|---------|
| `event-envelope.schema.json` | Wrapper for all control plane events |
| `profile-definition.schema.json` | User-authored profile configuration |
| `profile-selection.schema.json` | Profile activation events |
| `override-definition.schema.json` | Reusable override templates |
| `override-instance.schema.json` | Concrete override activations |
| `policy-composition.schema.json` | Materialized effective parameters |
| `delivery-request.schema.json` | Intent to deliver insulin |
| `delivery-observation.schema.json` | Confirmed delivery records |
| `reconciliation.schema.json` | Request/observation matching |
| `controller-kind-definition.schema.json` | Controller type capabilities |
| `controller-instance-registration.schema.json` | Controller instance registry |
| `capability-snapshot.schema.json` | Real-time controller state |
### Key Concepts
- **Config vs Runtime vs Computed** - Separate user-authored config from runtime activations from computed state
- **Events over Snapshots** - Append-only event streams with cursor-based sync
- **Authority Hierarchy** - Human > Agent > Controller for conflict resolution
- **Bridge Mode** - Synthesize events from legacy devicestatus uploads
- **MDI as First-Class** - Manual injections are always valid
## Recent Changes
- 2026-01-01: Added Agentic Control Plane RFC and JSON schemas
- 2025-12-31: Updated to version 15.0.4 (dev branch)
- Configured for Replit with INSECURE_USE_HTTP=true
- MongoDB 3.6.x driver