arc-agi-rs 0.1.0

🤖 A Rust client SDK for the ARC-AGI-3 API.
Documentation
// Copyright 2026 Mahmoud Harmouch.
//
// Licensed under the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! # Request Parameters
//!
//! This module provides fluent builder types for configuring all aspects of
//! [`crate::client::Client`] and its request methods.
//!
//! ## Parameter Builders
//!
//! | Builder | Purpose |
//! |---------|---------|
//! | [`ClientConfig`] | Top-level client configuration (API key, base URL, operation mode) |
//! | [`ScorecardParams`] | Parameters for opening a new scorecard |
//! | [`MakeParams`] | Parameters for creating an environment run via `make` |
//! | [`StepParams`] | Parameters for a single game step |
//!
//! ## Example
//!
//! ```rust
//! use arc_agi_rs::params::{ClientConfig, ScorecardParams};
//! use arc_agi_rs::models::OperationMode;
//!
//! let config = ClientConfig::new()
//!     .api_key("my-key")
//!     .base_url("https://three.arcprize.org")
//!     .operation_mode(OperationMode::Online);
//!
//! let sc_params = ScorecardParams::new()
//!     .source_url("https://github.com/wiseaidotdev/lmm/tree/main/lmm-agent")
//!     .tags(vec!["agent".to_string()]);
//!
//! assert_eq!(config.api_key.as_deref(), Some("my-key"));
//! assert_eq!(sc_params.tags.as_deref(), Some(&["agent".to_string()][..]));
//! ```
//!
//! ## See Also
//!
//! - [ARC-AGI-3 API Reference](https://three.arcprize.org)

use std::env;

use serde_json::{Map, Value};

use crate::models::OperationMode;

/// Top-level configuration for [`crate::client::Client`].
///
/// Construct with [`ClientConfig::new()`] and chain the fluent setter methods
/// before passing the instance to [`crate::client::ClientBuilder`].
///
/// Priority order (highest to lowest): builder methods → environment variables
/// → compiled-in defaults - mirroring the Python `Arcade.__init__` logic.
///
/// # Example
/// ```rust
/// use arc_agi_rs::params::ClientConfig;
///
/// let config = ClientConfig::new()
///     .api_key("abc-123")
///     .base_url("https://three.arcprize.org");
///
/// assert_eq!(config.api_key.as_deref(), Some("abc-123"));
/// ```
#[derive(Debug, Clone, Default)]
pub struct ClientConfig {
    /// ARC-AGI-3 API key (`ARC_API_KEY` env var fallback).
    pub api_key: Option<String>,
    /// Base URL of the ARC-AGI-3 server (`ARC_BASE_URL` env var fallback).
    pub base_url: Option<String>,
    /// Controls environment discovery strategy.
    pub operation_mode: OperationMode,
    /// Filesystem path scanned for local `metadata.json` files.
    pub environments_dir: Option<String>,
    /// Filesystem path where JSONL recordings are saved.
    pub recordings_dir: Option<String>,
}

impl ClientConfig {
    /// Creates a new, empty `ClientConfig` with all fields at their defaults.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the API key used in the `X-API-Key` header.
    pub fn api_key(mut self, key: impl Into<String>) -> Self {
        self.api_key = Some(key.into());
        self
    }

    /// Sets the base URL of the ARC-AGI-3 server.
    pub fn base_url(mut self, url: impl Into<String>) -> Self {
        self.base_url = Some(url.into());
        self
    }

    /// Sets the operation mode controlling environment discovery.
    pub fn operation_mode(mut self, mode: OperationMode) -> Self {
        self.operation_mode = mode;
        self
    }

    /// Sets the directory that is scanned for local environment `metadata.json` files.
    pub fn environments_dir(mut self, dir: impl Into<String>) -> Self {
        self.environments_dir = Some(dir.into());
        self
    }

    /// Sets the directory where JSONL recording files are saved.
    pub fn recordings_dir(mut self, dir: impl Into<String>) -> Self {
        self.recordings_dir = Some(dir.into());
        self
    }

    /// Returns the resolved base URL, falling back to the `ARC_BASE_URL`
    /// environment variable and then to the default production server URL.
    pub fn resolved_base_url(&self) -> String {
        self.base_url
            .clone()
            .or_else(|| env::var("ARC_BASE_URL").ok())
            .unwrap_or_else(|| "https://three.arcprize.org".to_string())
    }

    /// Returns the resolved API key, falling back to the `ARC_API_KEY`
    /// environment variable and then to an empty string.
    pub fn resolved_api_key(&self) -> String {
        self.api_key
            .clone()
            .or_else(|| env::var("ARC_API_KEY").ok())
            .unwrap_or_default()
    }
}

/// Parameters for opening a new ARC-AGI-3 scorecard.
///
/// Returned card IDs can be passed to subsequent [`crate::client::Client`]
/// calls that accept a `card_id`.
///
/// # Example
/// ```rust
/// use arc_agi_rs::params::ScorecardParams;
///
/// let params = ScorecardParams::new()
///     .source_url("https://github.com/wiseaidotdev/lmm")
///     .tags(vec!["agent".to_string()])
///     .competition_mode(false);
///
/// assert_eq!(params.tags.as_deref(), Some(&["agent".to_string()][..]));
/// ```
#[derive(Debug, Clone, Default)]
pub struct ScorecardParams {
    /// Optional URL linking to the agent or submission being evaluated.
    pub source_url: Option<String>,
    /// Classification tags for the scorecard (default: `["agent"]`).
    pub tags: Option<Vec<String>>,
    /// Freeform JSON-serialisable metadata stored alongside the scorecard.
    pub opaque: Option<Value>,
    /// When `true`, enforces one-way competition semantics on the server.
    pub competition_mode: Option<bool>,
}

impl ScorecardParams {
    /// Creates a new, empty `ScorecardParams`.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the source URL for the scorecard.
    pub fn source_url(mut self, url: impl Into<String>) -> Self {
        self.source_url = Some(url.into());
        self
    }

    /// Sets the classification tags for the scorecard.
    pub fn tags(mut self, tags: Vec<String>) -> Self {
        self.tags = Some(tags);
        self
    }

    /// Sets opaque metadata stored alongside the scorecard.
    pub fn opaque(mut self, data: Value) -> Self {
        self.opaque = Some(data);
        self
    }

    /// Enables or disables competition mode for the scorecard.
    pub fn competition_mode(mut self, enabled: bool) -> Self {
        self.competition_mode = Some(enabled);
        self
    }

    /// Serialises the params to a [`Value`] map suitable for sending as the
    /// JSON body of `POST /api/scorecard/open`.
    pub fn to_json_body(&self) -> Value {
        let mut map = Map::new();
        let tags = self
            .tags
            .clone()
            .unwrap_or_else(|| vec!["agent".to_string()]);
        map.insert(
            "tags".to_string(),
            Value::Array(tags.into_iter().map(Value::String).collect()),
        );
        if let Some(url) = &self.source_url {
            map.insert("source_url".to_string(), Value::String(url.clone()));
        }
        if let Some(opaque) = &self.opaque {
            map.insert("opaque".to_string(), opaque.clone());
        }
        if let Some(comp) = self.competition_mode {
            map.insert("competition_mode".to_string(), Value::Bool(comp));
        }
        Value::Object(map)
    }
}

/// Parameters for creating an environment run via
/// [`crate::client::Client::reset`].
///
/// # Example
/// ```rust
/// use arc_agi_rs::params::MakeParams;
///
/// let params = MakeParams::new("ls20", "sc-abc")
///     .seed(42)
///     .save_recording(true);
///
/// assert_eq!(params.game_id, "ls20");
/// assert_eq!(params.seed, 42);
/// assert!(params.save_recording);
/// ```
#[derive(Debug, Clone)]
pub struct MakeParams {
    /// Target game identifier (e.g. `"ls20"` or `"ls20-1234abcd"`).
    pub game_id: String,
    /// Scorecard to record this run under.
    pub scorecard_id: String,
    /// Optional existing run GUID to re-use.
    pub guid: Option<String>,
    /// Random seed for reproducible level ordering.
    pub seed: u32,
    /// When `true`, JSONL recordings are written to disk.
    pub save_recording: bool,
    /// When `true`, pixel frame data is included in the recording.
    pub include_frame_data: bool,
}

impl MakeParams {
    /// Creates a new `MakeParams` for the given game and scorecard.
    pub fn new(game_id: impl Into<String>, scorecard_id: impl Into<String>) -> Self {
        Self {
            game_id: game_id.into(),
            scorecard_id: scorecard_id.into(),
            guid: None,
            seed: 0,
            save_recording: false,
            include_frame_data: true,
        }
    }

    /// Sets an existing GUID to re-use rather than starting a fresh run.
    pub fn guid(mut self, guid: impl Into<String>) -> Self {
        self.guid = Some(guid.into());
        self
    }

    /// Sets the random seed for the run.
    pub fn seed(mut self, seed: u32) -> Self {
        self.seed = seed;
        self
    }

    /// Enables or disables writing JSONL recordings to disk.
    pub fn save_recording(mut self, save: bool) -> Self {
        self.save_recording = save;
        self
    }

    /// Enables or disables inclusion of pixel frame data in recordings.
    pub fn include_frame_data(mut self, include: bool) -> Self {
        self.include_frame_data = include;
        self
    }
}

/// Parameters for a single game step sent via
/// [`crate::client::Client::step`].
///
/// # Example
/// ```rust
/// use arc_agi_rs::params::StepParams;
/// use arc_agi_rs::serde_json::json;
///
/// let params = StepParams::new("ls20", "sc-abc", "run-guid-123", 1)
///     .data(json!({"x": 3, "y": 4}))
///     .reasoning(json!({"explanation": "moving right"}));
///
/// assert_eq!(params.action_id, 1);
/// ```
#[derive(Debug, Clone)]
pub struct StepParams {
    /// Game identifier.
    pub game_id: String,
    /// Scorecard identifier.
    pub scorecard_id: String,
    /// Run GUID from a prior `reset` call.
    pub guid: String,
    /// Numeric action identifier (0 = RESET).
    pub action_id: u32,
    /// Optional positional or key-value action data.
    pub data: Option<Value>,
    /// Optional freeform reasoning attached to this step.
    pub reasoning: Option<Value>,
}

impl StepParams {
    /// Creates a new `StepParams` for the given game step.
    pub fn new(
        game_id: impl Into<String>,
        scorecard_id: impl Into<String>,
        guid: impl Into<String>,
        action_id: u32,
    ) -> Self {
        Self {
            game_id: game_id.into(),
            scorecard_id: scorecard_id.into(),
            guid: guid.into(),
            action_id,
            data: None,
            reasoning: None,
        }
    }

    /// Attaches key-value action data (e.g. `{"x": 3, "y": 4}`).
    pub fn data(mut self, data: Value) -> Self {
        self.data = Some(data);
        self
    }

    /// Attaches freeform reasoning for this step.
    pub fn reasoning(mut self, reasoning: Value) -> Self {
        self.reasoning = Some(reasoning);
        self
    }
}

// Copyright 2026 Mahmoud Harmouch.
//
// Licensed under the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.