agent-first-http 0.7.0

Give an AI agent its own isolated browser to actually open a URL — running JavaScript when the page needs it and returning the page as files the agent can read — so it works from the real page instead of a search guess, an empty app shell, or a login wall, all without touching the browser you use every day.
Documentation
//! Newtype wrappers for identifiers used across the protocol — request IDs
//! returned to the agent, tab IDs scoped to a host's browser, and profile
//! names. Each type owns its own validation/generation so we can't accidentally
//! pass a profile name where a tab ID is expected.

use serde::{Deserialize, Serialize};

/// A per-fetch identifier, generated by the SDK on each `Client::fetch(...)`.
/// Goes into the response `request_id` field and into the default `--out`
/// path (`$TMPDIR/afhttp-out/<request_id>/`, platform equivalent).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct RequestId(pub String);

impl RequestId {
    #[must_use]
    pub fn new_v4() -> Self {
        Self(uuid::Uuid::new_v4().to_string())
    }

    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl std::fmt::Display for RequestId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

/// A browser tab identifier, assigned by the host's CDP target manager.
/// Architecture.md `§5` (`--tab new|<id>`) makes this the unit of fetch
/// affinity for reusing browser state across requests.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct TabId(pub String);

impl TabId {
    #[must_use]
    pub fn new(id: impl Into<String>) -> Self {
        Self(id.into())
    }

    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl std::fmt::Display for TabId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn request_id_is_uuid_v4_shaped() {
        let id = RequestId::new_v4();
        assert_eq!(id.as_str().len(), 36); // 8-4-4-4-12
        assert_eq!(id.as_str().matches('-').count(), 4);
    }

    #[test]
    fn ids_serialize_as_bare_strings() {
        let rid = RequestId("abc".into());
        let tid = TabId("page-42".into());
        assert_eq!(serde_json::to_string(&rid).unwrap_or_default(), "\"abc\"");
        assert_eq!(
            serde_json::to_string(&tid).unwrap_or_default(),
            "\"page-42\""
        );
    }
}