Version: 0.1 (Draft)
Date: March 2026
Status: Proposal
Depends on: Edge Contract Spec, Visual Primitives Spec, Stroke Format Spec
Used by: Tutor, Edge (via embedded Bridge library)
The Bridge Protocol defines how Tutor communicates with Edge. It translates Tutor's semantic intent into Edge's visual primitives and streams Edge's stroke data back to Tutor.
Bridge is NOT a hosted service. It is:
┌─────────────┐ ┌─────────────┐
│ TUTOR │ │ EDGE │
│ │ │ │
│ Semantic │◄─── Bridge Protocol ────►│ Primitives │
│ Commands │ (WebSocket) │ + Strokes │
│ │ │ │
└─────────────┘ └─────────────┘
| Goal | Implication |
|---|---|
| Semantic abstraction | Tutor says "highlight the error", not pixel coordinates |
| Capability adaptation | Bridge adapts output to Edge capabilities |
| Low latency | Direct WebSocket, no intermediate service |
| Offline resilience | Graceful degradation, reconnection, replay |
| Debuggable | Human-readable message format in development |
┌─────────────────────────────────────────────────────────────────┐
│ TUTOR DEVICE (Phone / Computer / Browser) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Tutor Core │ │
│ │ • Session logic │ │
│ │ • Behavior engine │ │
│ │ • Voice processing │ │
│ └────────────────────────┬──────────────────────────────────┘ │
│ │ Semantic commands │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Bridge Adapter │ │
│ │ • Translate semantic → primitives │ │
│ │ • Manage WebSocket connection │ │
│ │ • Handle capability adaptation │ │
│ └────────────────────────┬──────────────────────────────────┘ │
│ │ │
└───────────────────────────┼──────────────────────────────────────┘
│ WebSocket (WSS)
│
┌───────────────────────────┼──────────────────────────────────────┐
│ │ │
│ ┌────────────────────────▼──────────────────────────────────┐ │
│ │ Bridge Library │ │
│ │ • Parse incoming primitives │ │
│ │ • Adapt to device capabilities │ │
│ │ • Batch stroke data │ │
│ └────────────────────────┬──────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────▼──────────────────────────────────┐ │
│ │ Edge Core │ │
│ │ • Native rendering │ │
│ │ • Stroke capture │ │
│ └───────────────────────────────────────────────────────────┘ │
│ EDGE (Tablet) │
└─────────────────────────────────────────────────────────────────┘
| Component | Responsibilities |
|---|---|
| Tutor Core | Pedagogical decisions, session orchestration |
| Bridge Adapter (on Tutor) | Semantic → primitive translation, connection management |
| Bridge Library (on Edge) | Primitive parsing, capability adaptation, stroke batching |
| Edge Core | Native rendering, stroke capture |
First-time pairing:
┌─────────┐ ┌─────────┐
│ Tutor │ │ Edge │
└────┬────┘ └────┬────┘
│ │
│ User opens Tutor app │
│ "Scan QR or enter code" │
│ │
│ User opens Edge app │
│ Shows: QR + 6-digit │
│ code + local IP │
│◄───────────────────────────────────────│
│ │
│ User scans QR or enters code │
│ │
│ PAIR_REQUEST {code, tutorId} │
│───────────────────────────────────────►│
│ │
│ PAIR_ACCEPT {edgeId, publicKey} │
│◄───────────────────────────────────────│
│ │
│ Store pairing: edgeId ↔ tutorId │
│ │
Subsequent sessions (auto-pair):
┌─────────┐ ┌─────────┐
│ Tutor │ │ Edge │
└────┬────┘ └────┬────┘
│ │
│ Both apps open on same network │
│ │
│ mDNS discovery / stored IP │
│───────────────────────────────────────►│
│ │
│ RECONNECT {tutorId, sessionToken} │
│───────────────────────────────────────►│
│ │
│ CAPABILITIES {...} │
│◄───────────────────────────────────────│
│ │
│ SESSION_CONFIG {...} │
│───────────────────────────────────────►│
│ │
| Transport | Use Case |
|---|---|
| WebSocket (WSS) | Primary, real-time bidirectional |
| Local WebSocket | Same WiFi, lower latency |
| Cloud relay | Fallback when direct connection fails |
Connection preference order:
┌──────────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────┐ Pair ┌──────────┐ │
│ │ UNPAIRED│────────────────►│ PAIRED │ │
│ └─────────┘ └────┬─────┘ │
│ ▲ │ │
│ │ Unpair │ Connect │
│ │ ▼ │
│ ┌─────────┐ Disconnect ┌──────────┐ Start session │
│ │ IDLE │◄────────────────│CONNECTED │─────────────────┐ │
│ └─────────┘ └──────────┘ │ │
│ ▲ ▲ │ │
│ │ │ Reconnect │ │
│ │ │ ▼ │
│ │ ┌──────────┐ ┌────────┐│
│ │ │RECONNECT │◄───────────│ ACTIVE ││
│ │ └──────────┘ Conn lost └───┬────┘│
│ │ │ │
│ │ End session │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
Tutor speaks in semantic commands. Bridge translates to primitives.
SEMANTIC COMMANDS
├── Session
│ ├── START_SESSION
│ ├── END_SESSION
│ ├── PAUSE_SESSION
│ └── RESUME_SESSION
│
├── Exercise
│ ├── LOAD_EXERCISE
│ ├── NEXT_EXERCISE
│ └── PREVIOUS_EXERCISE
│
├── Annotation
│ ├── HIGHLIGHT_REGION
│ ├── HIGHLIGHT_ERROR
│ ├── HIGHLIGHT_SUCCESS
│ ├── POINT_AT
│ ├── CIRCLE
│ ├── BRACKET
│ └── STRIKETHROUGH
│
├── Feedback
│ ├── MARK_CORRECT
│ ├── MARK_INCORRECT
│ ├── MARK_PARTIAL
│ └── PULSE_ATTENTION
│
├── Hint
│ ├── SHOW_HINT
│ ├── HIDE_HINT
│ └── POINT_HINT
│
└── Control
├── CLEAR_ANNOTATIONS
├── CLEAR_HINTS
└── CLEAR_FEEDBACK
START_SESSION
{
"command": "START_SESSION",
"params": {
"studentId": "stu_abc123",
"courseId": "course_algebra1",
"resumeFrom": "ex_4520"
}
}
Bridge actions:
END_SESSION
{
"command": "END_SESSION",
"params": {
"reason": "completed",
"saveProgress": true
}
}
LOAD_EXERCISE
{
"command": "LOAD_EXERCISE",
"params": {
"exerciseId": "ex_4521",
"content": {
"type": "equation_solve",
"instruction": "Solve for x:",
"equation": "3x + 5 = 14",
"workZone": true,
"answerZone": true
}
}
}
Bridge translates to primitives:
{
"type": "LOAD_EXERCISE",
"payload": {
"exerciseId": "ex_4521",
"primitives": [
{"type": "TEXT", "id": "instr", "content": "Solve for x:", ...},
{"type": "MATH_BLOCK", "id": "eq", "content": "3x + 5 = 14", ...},
{"type": "INPUT_ZONE", "id": "work", "semantic": "work", ...},
{"type": "INPUT_ZONE", "id": "answer", "semantic": "answer", ...}
]
}
}
HIGHLIGHT_REGION
{
"command": "HIGHLIGHT_REGION",
"params": {
"target": {"zone": "work", "strokeIds": ["str_042", "str_043"]},
"intent": "focus"
}
}
Bridge calculates bounding box from stroke IDs, sends HIGHLIGHT primitive.
HIGHLIGHT_ERROR
{
"command": "HIGHLIGHT_ERROR",
"params": {
"target": {"zone": "work", "stepIndex": 2},
"message": "Check the sign here"
}
}
Bridge translates to:
intent: "error"style: "wavy"POINT_AT
{
"command": "POINT_AT",
"params": {
"target": {"zone": "work", "position": {"x": 0.35, "y": 0.42}},
"from": "above"
}
}
Bridge translates to ARROW primitive.
MARK_CORRECT
{
"command": "MARK_CORRECT",
"params": {
"target": {"zone": "answer"}
}
}
Bridge translates to:
intent: "success"markType: "correct"MARK_INCORRECT
{
"command": "MARK_INCORRECT",
"params": {
"target": {"zone": "answer"},
"showCorrect": false
}
}
SHOW_HINT
{
"command": "SHOW_HINT",
"params": {
"content": "What operation undoes addition?",
"anchor": {"zone": "work", "stepIndex": 1},
"position": "above"
}
}
Bridge translates to HINT_BUBBLE primitive.
Edge Bridge (on Edge) Tutor
│ │ │
│ Raw strokes │ │
│──────────────────────────►│ │
│ │ │
│ │ Batch + normalize │
│ │ │
│ │ STROKE_BATCH │
│ │──────────────────────►│
│ │ │
│ │ │ Vision
│ │ │ parsing
│ │ │
│ │ STROKE_ACK │
│ │◄──────────────────────│
│ │ │
Bridge Library on Edge adds metadata before sending:
{
"type": "STROKE_BATCH",
"payload": {
"sessionId": "sess_xyz",
"seq": 42,
"strokes": [
{
"id": "str_042",
"ts": 1711670500000,
"pts": [...],
"zone": "work",
"bounds": {"x": 0.12, "y": 0.35, "w": 0.18, "h": 0.06}
}
]
}
}
Added fields:
zone: Which INPUT_ZONE contains this strokebounds: Bounding box (helps Tutor target annotations)Tutor can reference strokes in commands:
{
"command": "HIGHLIGHT_ERROR",
"params": {
"target": {"strokeIds": ["str_042", "str_043"]}
}
}
Bridge maintains stroke → position mapping to calculate highlight bounds.
Bridge Adapter (on Tutor) adapts commands based on Edge capabilities:
| Capability | Adaptation |
|---|---|
color: false |
Convert intent colors to patterns |
refreshRate: slow |
Batch visual updates, skip animations |
stylus: false |
Hide INPUT_GUIDE, adjust INPUT_ZONE |
Color → Grayscale:
Command: HIGHLIGHT_ERROR
│
├─► Edge has color
│ └─► HIGHLIGHT {intent: "error"} → Red fill
│
└─► Edge no color
└─► HIGHLIGHT {intent: "error"} → Hatch pattern + thick border
Animation → Static:
Command: PULSE_ATTENTION
│
├─► Edge refreshRate: fast
│ └─► PULSE → Animated expanding circle
│
└─► Edge refreshRate: slow
└─► PULSE → Single inverted flash
When Edge can't render something:
| Error | Tutor Response |
|---|---|
| WebSocket disconnect | Auto-reconnect (exponential backoff) |
| Edge unresponsive | Voice: "I lost connection to your notebook" |
| Pairing lost | Prompt re-pairing |
| Error | Bridge Response |
|---|---|
| Unknown command | Log, ignore |
| Malformed primitive | Skip primitive, continue |
| Missing stroke reference | Use fallback position |
Edge sends ERROR messages:
{
"type": "ERROR",
"payload": {
"code": "RENDER_FAILED",
"primitiveId": "math_001",
"message": "Unsupported LaTeX: \\frac"
}
}
Bridge Adapter can:
| State | Owner | Sync |
|---|---|---|
| Exercise content | Tutor | Push to Edge on load |
| Student strokes | Edge | Stream to Tutor |
| Annotations | Tutor | Push to Edge |
| Session progress | Tutor | Persist to Cloud |
Strokes are append-only. No conflicts possible.
Annotations are replace-only. Last command wins.
Exercise state is Tutor-authoritative. Edge never modifies.
On reconnect:
// Edge reconnection message
{
"type": "RECONNECT_STATE",
"payload": {
"sessionId": "sess_xyz",
"exerciseId": "ex_4521",
"strokeCount": 87,
"lastSeq": 42
}
}
// Tutor response if mismatch
{
"type": "SYNC_REQUEST",
"payload": {
"replayStrokesFrom": 35,
"resendExercise": true
}
}
| Path | Target | Max |
|---|---|---|
| Stroke capture → Tutor | 250ms | 500ms |
| Tutor command → Edge render | 150ms | 300ms |
| Reconnection | 2s | 5s |
| Exercise load | 500ms | 1s |
| Metric | Target |
|---|---|
| Stroke batches/sec | 5 (200ms interval) |
| Points/batch | Up to 100 |
| Commands/sec | Up to 10 |
| Concurrent sessions/Tutor | 1 (single student) |
| Scenario | Bandwidth |
|---|---|
| Active writing | ~10 KB/s |
| Idle | <1 KB/s (heartbeat only) |
| Exercise load | ~5-20 KB burst |
| Message | Purpose |
|---|---|
SESSION_CONFIG |
Configure session parameters |
LOAD_EXERCISE |
Load exercise content |
PRIMITIVE |
Single visual primitive |
PRIMITIVE_BATCH |
Multiple primitives |
CLEAR |
Clear layer |
STROKE_ACK |
Acknowledge strokes |
SESSION_END |
End session |
SYNC_REQUEST |
Request state sync |
| Message | Purpose |
|---|---|
CAPABILITIES |
Device capabilities |
STROKE_BATCH |
Stroke data |
GESTURE |
User gesture |
HEARTBEAT |
Status update |
ZONE_ACTIVITY |
Input zone enter/exit |
ERROR |
Error report |
RECONNECT_STATE |
State on reconnect |
| Command | Translates to |
|---|---|
START_SESSION |
SESSION_CONFIG + LOAD_EXERCISE |
LOAD_EXERCISE |
LOAD_EXERCISE (with primitives) |
HIGHLIGHT_REGION |
HIGHLIGHT |
HIGHLIGHT_ERROR |
HIGHLIGHT + UNDERLINE + HINT_BUBBLE |
MARK_CORRECT |
HIGHLIGHT + MARK |
SHOW_HINT |
HINT_BUBBLE |
CLEAR_ANNOTATIONS |
CLEAR {target: "annotations"} |
Time Tutor Bridge Edge
─────────────────────────────────────────────────────────────────────────
0.0s START_SESSION ─────────────────►
SESSION_CONFIG ─────────────────►
LOAD_EXERCISE ──────────────────►
Render
0.5s Voice: "Let's continue
with algebra..."
◄─── STROKE_BATCH
2.0s Parse: "3x"
(wait, incomplete)
◄─── STROKE_BATCH
3.5s Parse: "3x + 5 = 14"
(equation, wait for work)
◄─── STROKE_BATCH
5.0s Parse: "3x = 9" ✓
(Tutor: correct step,
stay quiet)
◄─── STROKE_BATCH
6.5s Parse: "x = 3" ✓
MARK_CORRECT ──────────────────►
HIGHLIGHT ─────────────────────►
MARK ───────────────────────────►
Render ✓
Voice: "Nice work!"
| Capability | BOOX | reMarkable | iPad | Web |
|---|---|---|---|---|
| color | ✓ (some) | ✗ | ✓ | ✓ |
| refreshRate | medium | slow | fast | fast |
| stylusPressure | ✓ | ✓ | ✓ | ✗ |
| stylusTilt | ✓ | ✓ | ✓ | ✗ |
| audio | ✓ (some) | ✗ | ✓ | ✓ |
| haptics | ✗ | ✗ | ✓ | ✗ |
Next spec: Vision API (strokes → structured math)