areev 0.2.0

Rust SDK for the Areev knowledge database — gRPC and HTTP transports
Documentation
# Areev Rust SDK

Rust client library for the [Areev](https://areev.ai) knowledge database.

A **grain** is one typed memory fact — the atomic unit Areev stores (e.g. the
Fact "John likes coffee"). Everything in this SDK ultimately reads or writes
grains.

## Installation

```toml
[dependencies]
areev = "1"
```

For gRPC transport:
```toml
[dependencies]
areev = { version = "1", features = ["grpc"] }
```

## Quick Start

```rust
use areev::Areev;

#[tokio::main]
async fn main() -> areev::Result<()> {
    let areev = Areev::from_env(); // reads AREEV_API_KEY, AREEV_URL from env

    areev.remember("John likes coffee").await?;

    let results = areev.recall("what does John like?").await?;
    for hit in &results.results {
        println!("{}: {:?}", hit.grain_type, hit.fields);
    }

    Ok(())
}
```

### Configuration

The client reads from environment variables by default:

| Variable | Default | Description |
|----------|---------|-------------|
| `AREEV_API_KEY` | — | API key (sent as `Authorization: Bearer <key>`) |
| `AREEV_URL` | `https://app.areev.ai` | Server endpoint |
| `AREEV_MEMORY_ID` | `default` | Memory database ID |

Or pass them explicitly:

```rust
let areev = Areev::new(
    Some("ar_..."),                // api_key
    Some("https://dub.areev.ai"), // url
    Some("my-memory"),            // memory_id
);
```

## API

The top-level [`Areev`] client exposes two bare verbs (`remember` /
`recall`) plus a resource accessor for every domain. The namespaces split
into two groups.

> **Known limitation (v1.0).** Most resource-layer methods return
> `serde_json::Value` rather than typed response structs — reach into the
> `Value` for fields. A handful of surfaces are already typed (e.g.
> `recall()` results, `connectors()`). Typed response models for the
> remaining resources (recall/chat/harness → structs) land in **v1.1**.

**Core** — what most integrations use day to day:

| Entry point | What it's for |
|-------------|---------------|
| `remember(text)` / `recall(query)` | Store NL memory (LLM extracts structure) / search it |
| `grains()` | Typed grain CRUD: `add`, `get`, `supersede`, `forget`, `recall`, `cal`, … |
| `harness()` | Flow-A chat: `chat`, `chat_resume`, `chat_cancel`, `chat_session`/`chat_interactive` |
| `tools()` | v1 tools surface — bind/list/invoke tools on a harness |
| `connectors()` | Third-party connector catalog + OAuth (Axtion-backed): `list`, `actions`, `authorize`, `poll_oauth`, `store_credentials` |
| `memories()` | Memory-database lifecycle + stats |
| `system()` | `health`, `stats`, `config`, `models` |

**Advanced / Compliance** — governance, identity, and ingestion surfaces
you reach for as you scale: `compliance()` (GDPR/CCPA audit, export,
verify), `consent()`, `scope()`, `policy()`, `authz()`,
`agent_identities()`, `sessions()`, `namespaces()`, `provenance()`,
`preferences()`, `hooks()`, `goals()`, `chat()`, `connections()`,
`knowledge_sources()`, `imports()`.

```rust
let areev = areev::Areev::from_env();

// Typed grain ops via the grains resource.
let hash = areev.grains().add(
    "fact",
    serde_json::json!({"subject": "john", "relation": "likes", "object": "coffee"}),
    None,
).await?;
let grain = areev.grains().get(&hash).await?;

// System ops.
let _health = areev.system().health().await?;
```

### Full flow

connect → remember/recall → create a harness and chat → register a
client-side tool → connect a third-party connector and bind one of its
actions as a tool — without hand-writing a single JSON Schema:

```rust
use std::pin::Pin;
use areev::{Areev, ChatExecutors};
use serde_json::json;

#[tokio::main]
async fn main() -> areev::Result<()> {
    let areev = Areev::new(
        Some("ap_local_..."),            // ap_*/ar_* key
        Some("http://localhost:4210"),   // your cell
        Some("my-first-memory"),
    );

    // 1. Remember + recall
    areev.remember("Ada prefers email over Slack").await?;
    let hits = areev.recall("how should I contact Ada?").await?;
    for h in &hits.results {
        println!("{} {:?}", h.grain_type, h.fields);
    }

    // 2. Create a harness wired to a provider, then chat
    areev
        .harness()
        .create("Sales Bot", "sales-bot")
        .description("Outbound sales assistant")
        .llm_config("openai", "gpt-4o-mini")
        // provider_type defaults to provider_id ("openai") automatically.
        .send()
        .await?;
    let reply = areev.harness().chat("sales-bot", "Say hi").send().await?;
    println!("{reply}");

    // 3. Add a client-side tool (executed in your process)
    let mut executors: ChatExecutors = ChatExecutors::new();
    executors.insert("get_weather", Box::new(|_name, args| {
        Pin::from(Box::new(async move {
            let city = args.get("city").and_then(|v| v.as_str()).unwrap_or("?");
            Ok(json!({ "city": city, "temp_c": 22 }))
        }))
    }));
    areev
        .harness()
        .chat_session("sales-bot")
        .message("What's the weather in Paris?")
        .executors(&executors)
        .send()
        .await?;

    // 4. Connect a third-party connector (OAuth), then poll for completion
    let flow = areev
        .connectors()
        .authorize("gmail", "https://yourapp.example/oauth/cb")
        .await?;
    println!("Open this URL to authorize: {}", flow.authorize_url);
    // ... user consents in the browser, provider redirects back ...
    // Poll until the flow completes — sleep between attempts so you don't
    // hammer the endpoint.
    loop {
        let status = areev.connectors().poll_oauth("gmail", &flow.state).await?;
        match status.get("status").and_then(|s| s.as_str()) {
            Some("success") => break,
            Some("error") | Some("expired") => {
                eprintln!("oauth flow failed: {status}");
                break;
            }
            _ => tokio::time::sleep(std::time::Duration::from_secs(2)).await,
        }
    }

    // 5. Bind a connector action as a tool — schema fetched for you
    areev
        .tools()
        .bind_axtion("sales-bot", "gmail", "send-email") // action key from connectors().actions()
        .description("Send an email via Gmail")
        .send()
        .await?;

    Ok(())
}
```

### Connectors

`connectors()` is the catalog + OAuth surface (backed by Axtion). All
methods return clean, parsed data — provider tokens never touch the SDK.

> **Naming, disambiguated.** `connectors()` is the third-party connector
> *catalog* + OAuth (Gmail, Slack, …); `connections()` manages a memory's
> live data-source *connections* (Knowledge Sources); `chat()` is the
> stateless completions endpoint; and `harness()` is the stateful
> LLM-plus-tools runtime you create, bind tools to, and converse with.

| Method | Returns |
|--------|---------|
| `connectors().list()` | `Vec<ConnectorSpec>` — `{name, display_name, category, version}` |
| `connectors().get(name)` | Raw connector detail (metadata + action descriptors) |
| `connectors().actions(name)` | `Vec<ConnectorAction>` — `{name, display_name, description, param_schema, required}` |
| `connectors().authorize(name, redirect_uri)` | `AuthorizeResponse` — `{authorize_url, state, ..}` |
| `connectors().poll_oauth(name, state)` | Raw `{status: "pending"\|"success"\|"error"\|"expired", ..}` |
| `connectors().store_credentials(body)` | Store API-key credentials (non-OAuth connectors) |

```rust
let catalog = areev.connectors().list().await?;
// → [ConnectorSpec { name: "gmail", display_name: "Google Gmail",
//     category: "Communication", version: "1.0.0" }, ...]

let actions = areev.connectors().actions("gmail").await?;
// → [ConnectorAction { name: "send-email", display_name: "Send Email",
//     param_schema: {...}, required: ["to", "subject", "body"] }, ...]
```

> **Prerequisite — redirect-uri allowlist.** The `redirect_uri` you pass to
> `connectors().authorize(...)` must be **pre-registered on your org** by an
> admin. An unregistered URI is rejected before any provider redirect — the
> call raises [`AreevError::Validation`] HTTP 400. Register your callback
> URL(s) in org settings before going live.

`tools().bind_axtion(slug, connector, action)` then looks up the action's
parameter schema from `connectors().actions()` and binds it for you — so
the agent gets a fully-typed tool without you writing any JSON Schema.

### Harness Chat

Use `harness().chat_session(slug)` to drive a harness (Areev's
LLM-plus-tools runtime) with client-side tool executors. The builder runs
the pause/resume loop for you when the model calls a `client://` tool, and
names each argument so the call site stays readable:

```rust
use std::pin::Pin;
use areev::{Areev, ChatExecutors};
use serde_json::json;

let areev = Areev::from_env();

let mut executors: ChatExecutors = ChatExecutors::new();
executors.insert("get_weather", Box::new(|_name, args| {
    Pin::from(Box::new(async move {
        let city = args.get("city").and_then(|v| v.as_str()).unwrap_or("unknown");
        Ok(json!({ "city": city, "temp_c": 22, "conditions": "sunny" }))
    }))
}));

let response = areev
    .harness()
    .chat_session("weather-harness")
    .conversation_id("conv-1") // omit for a fresh single-shot turn
    .message("What's the weather in Paris?")
    .executors(&executors)
    .send()
    .await?;

println!("{}", response.text);
```

`harness().chat_interactive(slug, conversation_id, message, &executors)`
is the equivalent positional form. `harness().chat`,
`harness().chat_resume`, and `harness().chat_cancel` are the low-level
primitives if you want to run the loop yourself.

## Transports

| Transport | Feature | Default |
|-----------|---------|---------|
| HTTP/REST | `http` | Yes |
| gRPC | `grpc` | No |

## License

BUSL-1.1