areev 0.1.1

Rust SDK for the Areev knowledge database — gRPC and HTTP transports
Documentation
use std::collections::HashMap;
use std::env;
use std::future::Future;
use std::pin::Pin;

use crate::error::{AreevError, Result};
use crate::http::HttpClient;
use crate::types::*;

const DEFAULT_URL: &str = "https://app.areev.ai";
const DEFAULT_MEMORY: &str = "default";

/// High-level Areev client with minimal boilerplate.
///
/// # Examples
///
/// ```no_run
/// # #[tokio::main]
/// # async fn main() -> areev::Result<()> {
/// use areev::Areev;
///
/// let areev = Areev::from_env();
/// areev.remember("John likes coffee").await?;
/// let results = areev.recall("what does John like?").await?;
/// # Ok(())
/// # }
/// ```
pub struct Areev {
    inner: HttpClient,
}

impl Areev {
    /// Create a client from environment variables.
    ///
    /// | Variable | Default | Description |
    /// |----------|---------|-------------|
    /// | `AREEV_API_KEY` | — | API key (`X-API-Key` header) |
    /// | `AREEV_URL` | `https://app.areev.ai` | Server endpoint |
    /// | `AREEV_MEMORY_ID` | `default` | Memory database ID |
    pub fn from_env() -> Self {
        let api_key = env::var("AREEV_API_KEY").ok();
        let url = env::var("AREEV_URL").unwrap_or_else(|_| DEFAULT_URL.to_string());
        let memory_id = env::var("AREEV_MEMORY_ID").unwrap_or_else(|_| DEFAULT_MEMORY.to_string());
        Self {
            inner: HttpClient::new(&url, &memory_id, api_key.as_deref()),
        }
    }

    /// Create a client from explicit values.
    pub fn new(api_key: Option<&str>, url: Option<&str>, memory_id: Option<&str>) -> Self {
        Self {
            inner: HttpClient::new(
                url.unwrap_or(DEFAULT_URL),
                memory_id.unwrap_or(DEFAULT_MEMORY),
                api_key,
            ),
        }
    }

    // -- Primary methods --------------------------------------------------

    /// Store a natural-language memory. Extracts beliefs synchronously.
    pub async fn remember(&self, text: &str) -> Result<RememberResponse> {
        let req = RememberRequest {
            text: text.to_string(),
            sync: Some(true),
            keep_source: None,
            namespace: None,
            user_id: None,
            tags: None,
            source_type: None,
            created_at: None,
            confidence: None,
            extract_event_date: None,
            auto_relate: None,
        };
        self.inner.remember(&req).await
    }

    /// Search memories. Returns matching grains.
    pub async fn recall(&self, query: &str) -> Result<RecallResponse> {
        let req = RecallRequest {
            query: Some(query.to_string()),
            limit: Some(10),
            ..Default::default()
        };
        self.inner.recall(&req).await
    }

    /// Delete a memory by hash.
    pub async fn forget(&self, hash: &str) -> Result<()> {
        self.inner.forget(hash).await
    }

    // -- Low-level methods ------------------------------------------------

    /// Add a typed grain.
    pub async fn add(&self, req: &AddRequest) -> Result<AddResponse> {
        self.inner.add(req).await
    }

    /// Get a grain by hash.
    pub async fn get(&self, hash: &str) -> Result<GetResponse> {
        self.inner.get(hash).await
    }

    /// Supersede (update) a grain.
    pub async fn supersede(&self, req: &SupersedeRequest) -> Result<SupersedeResponse> {
        self.inner.supersede(req).await
    }

    /// Health check.
    pub async fn health(&self) -> Result<HealthResponse> {
        self.inner.health().await
    }

    /// Database statistics.
    pub async fn stats(&self) -> Result<StatsResponse> {
        self.inner.stats().await
    }

    /// Flush write buffer.
    pub async fn flush(&self) -> Result<()> {
        self.inner.flush().await
    }

    // -- Harness chat (HPL) ---------------------------------------------------

    /// Run one harness turn (Flow-A).
    pub async fn harness_chat(
        &self,
        slug: &str,
        conversation_id: &str,
        user_message: &str,
    ) -> Result<HarnessChatResponse> {
        let req = HarnessChatRequest {
            conversation_id: conversation_id.to_string(),
            user_message: user_message.to_string(),
            model: None,
            provider: None,
        };
        self.inner.harness_chat(slug, &req).await
    }

    /// Resume a paused harness turn with tool outputs.
    pub async fn harness_chat_resume(
        &self,
        slug: &str,
        req: &ChatResumeRequest,
    ) -> Result<HarnessChatResponse> {
        self.inner.harness_chat_resume(slug, req).await
    }

    /// Cancel a paused harness chat session. Idempotent.
    pub async fn cancel_harness_chat_session(
        &self,
        slug: &str,
        session_id: &str,
    ) -> Result<()> {
        self.inner
            .cancel_harness_chat_session(slug, session_id)
            .await
    }

    /// Interactive harness chat helper.
    ///
    /// Runs one turn via `chat()`; if it pauses on `client://` tool calls,
    /// looks up an executor for each pending `tool_name` in `executors`,
    /// runs them, and resumes the turn. Loops until `status == Completed`.
    ///
    /// # Errors
    ///
    /// - Returns `AreevError::Api { status: 500, .. }` if a pending tool has
    ///   no registered executor in `executors`.
    /// - Any HTTP error from the underlying calls is propagated.
    pub async fn chat_interactive<E>(
        &self,
        slug: &str,
        conversation_id: &str,
        user_message: &str,
        executors: &ChatExecutors<E>,
    ) -> Result<HarnessChatResponse>
    where
        E: Fn(
                String,
                serde_json::Value,
            )
                -> Pin<Box<dyn Future<Output = Result<serde_json::Value>> + Send>>
            + Send
            + Sync,
    {
        let mut resp = self.harness_chat(slug, conversation_id, user_message).await?;
        loop {
            if matches!(resp.status, HarnessChatStatus::Completed) {
                return Ok(resp);
            }
            let session_id = resp.session_id.clone().ok_or_else(|| AreevError::Api {
                status: 500,
                message: "requires_action without session_id".into(),
            })?;
            let mut tool_outputs = Vec::with_capacity(resp.pending_tool_calls.len());
            for pc in &resp.pending_tool_calls {
                let executor = executors.get(&pc.tool_name).ok_or_else(|| AreevError::Api {
                    status: 500,
                    message: format!(
                        "no executor registered for tool '{}' (tool_call_id={})",
                        pc.tool_name, pc.tool_call_id
                    ),
                })?;
                let args: serde_json::Value =
                    serde_json::from_str(&pc.arguments).unwrap_or(serde_json::Value::Null);
                let output = executor(pc.tool_name.clone(), args).await?;
                tool_outputs.push(ChatToolOutput {
                    tool_call_id: pc.tool_call_id.clone(),
                    output,
                    is_error: None,
                });
            }
            let resume_req = ChatResumeRequest {
                session_id,
                tool_outputs,
                tenant_timestamp_ms: None,
            };
            resp = self.harness_chat_resume(slug, &resume_req).await?;
        }
    }
}

/// Map of tool-name → async executor used by [`Areev::chat_interactive`].
///
/// Each executor takes `(tool_name, arguments)` (arguments already JSON-parsed
/// from the LLM's raw string) and must return the structured output that
/// will be POSTed back to `/chat/resume`.
pub type ChatExecutor = Box<
    dyn Fn(
            String,
            serde_json::Value,
        ) -> Pin<Box<dyn Future<Output = Result<serde_json::Value>> + Send>>
        + Send
        + Sync,
>;

/// Registry of client-side tool executors keyed by `tool_name`.
pub struct ChatExecutors<E = ChatExecutor> {
    inner: HashMap<String, E>,
}

impl<E> Default for ChatExecutors<E> {
    fn default() -> Self {
        Self {
            inner: HashMap::new(),
        }
    }
}

impl<E> ChatExecutors<E> {
    pub fn new() -> Self {
        Self::default()
    }

    /// Register an executor for a tool name. Overwrites any prior entry.
    pub fn insert(&mut self, name: impl Into<String>, exec: E) -> &mut Self {
        self.inner.insert(name.into(), exec);
        self
    }

    pub fn get(&self, name: &str) -> Option<&E> {
        self.inner.get(name)
    }

    pub fn len(&self) -> usize {
        self.inner.len()
    }

    pub fn is_empty(&self) -> bool {
        self.inner.is_empty()
    }
}