nomad-common 0.1.0

Shared types and protocol for Nomad web extraction engine
Documentation
use serde::{Deserialize, Serialize};

use crate::ecs::{ChangedEntities, EcsFrame, PageMetadata};

/// Structured error codes for machine-readable error handling.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
    Unknown = 0,
    Timeout = 1,
    TabNotFound = 2,
    RateLimited = 3,
    BadRequest = 4,
    Internal = 5,
}

/// Messages sent over Unix Domain Socket between daemon and client.
///
/// ⚠ DISCRIMINANT ORDER IS FIXED — never reorder, never insert between groups.
/// Adding new variants: APPEND to the end.
/// Reordering WILL silently break all existing clients via bincode encoding.
///
/// Current layout:
///   Daemon→Client: Frame=0, FrameDelta=1, Metadata=2, Error=3, EvaluateResult=4, FoundEntities=5
///   Client→Daemon: Navigate=6, Scroll=7, Click=8, Input=9, Resize=10,
///                  NewTab=11, CloseTab=12, GoBack=13, GoForward=14, Evaluate=15
///   Agent:         FindEntities=16, EntitySnapshot=17, ClickEntity=18, InputEntity=19
///   Extensions:    ActionLog=20, ClickSelector=21, GetActionLog=22, CompareTabs=23, TabComparison=24, TransferTab=25
///   Health:        Health=26, HealthResult=27
///   Auth/Version:  ApiKey=28, Handshake=29
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum DaemonMessage {
    // ── Daemon → Client (0-5) ──
    Frame { tab_id: u64, frame: EcsFrame, metadata: PageMetadata },
    FrameDelta { tab_id: u64, sequence_number: u64, scroll_offset: (f32, f32), viewport: (f32, f32), entities: ChangedEntities },
    Metadata { tab_id: u64, title: String, url: String },
    Error { tab_id: u64, code: ErrorCode, description: String },
    EvaluateResult { tab_id: u64, result: String },
    FoundEntities { tab_id: u64, entities: Vec<EntitySummary> },

    // ── Client → Daemon (6-15) ──
    Navigate { tab_id: u64, url: String, format: u8 },
    Scroll { tab_id: u64, dx: f32, dy: f32 },
    Click { tab_id: u64, x: f32, y: f32 },
    Input { tab_id: u64, text: String },
    Resize { tab_id: u64, width: u32, height: u32 },
    NewTab { url: Option<String> },
    CloseTab { tab_id: u64 },
    GoBack { tab_id: u64 },
    GoForward { tab_id: u64 },
    Evaluate { tab_id: u64, js: String, timeout_ms: u32 },

    // ── Agent Messages (16-19) ──
    FindEntities { tab_id: u64, query: EntityQuery },
    EntitySnapshot { tab_id: u64 },
    ClickEntity { tab_id: u64, entity_id: u64 },
    InputEntity { tab_id: u64, entity_id: u64, value: String },

    // ── Extensions (20+) — APPEND ONLY, never insert ──

    /// Daemon → Client: action history for agent reasoning.
    ActionLog { tab_id: u64, entries: Vec<ActionEntry> },

    /// Click the first entity matching a semantic selector.
    ClickSelector { tab_id: u64, query: EntityQuery },

    /// Get action history for a tab.
    GetActionLog { tab_id: u64 },

    /// Compare two tabs' entity states.
    CompareTabs { tab_a: u64, tab_b: u64 },
    /// Daemon → Client: result of CompareTabs.
    TabComparison { tab_a: u64, tab_b: u64, summary: String },

    /// Transfer focus / content from one tab to another.
    TransferTab { from: u64, to: u64 },

    /// Client → Daemon: request daemon health/status info.
    Health,
    /// Daemon → Client: health/status response.
    HealthResult { pid: u32, uptime_secs: u64, rss_kb: u64, connections: u32, version: String },

    /// Client → Daemon: authenticate with API key (must be first message).
    ApiKey { key: String },
    /// Bidirectional: protocol version handshake.
    Handshake { version: u32, capabilities: Vec<String> },
}

/// Single entry in the action history for agent reasoning.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ActionEntry {
    pub sequence: u64,
    pub action: String,
    pub target_id: u64,
    pub value: String,
    pub timestamp: u64,
    pub entity_count: u32,
}

/// Filter for finding entities by semantic properties.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct EntityQuery {
    pub role: Option<String>,
    pub tag: Option<String>,
    pub intent: Option<String>,
    pub text_contains: Option<String>,
    pub label: Option<String>,
    pub limit: Option<u16>,
}

/// Summary of a single entity for agent consumption.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct EntitySummary {
    pub id: u64,
    pub tag: String,
    pub text: String,
    pub role: String,
    pub href: String,
    pub classes: String,
    pub intent: String,
    pub is_interactive: bool,
    pub parent_id: u64,
}