llmrix-rust-sdk 1.0.0

Official Rust SDK for the llmrix AI Agent Platform API
Documentation
# llmrix-rust-sdk

Official Rust SDK for [llmrix](https://github.com/llmrix/llmrix-rust-sdk) — AI Agent Platform.

- Rust 1.75+ (MSRV)
- Async/await with `tokio` runtime
- SSE streaming via `reqwest` + `futures-util`
- Fully typed with `serde` serialization
- `thiserror`-based error hierarchy

---

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
llmrix-rust-sdk = "1.0"
tokio = { version = "1", features = ["full"] }
```

---

## Quick Start

```rust
use llmrix_sdk::{
    LlmrixClient,
    model::ConversationCreateRequest,
    streaming::event::StreamEvent,
};

#[tokio::main]
async fn main() -> llmrix_sdk::error::Result<()> {
    // 1. Build the client (cheap to clone — shares the connection pool)
    let client = LlmrixClient::builder()
        .base_url("https://www.llmrix.com")
        .api_key("sk-xxx")
        .build()?;

    // 2. Create a conversation
    let conv = client
        .conversations()
        .create(ConversationCreateRequest {
            title: "My Chat".into(),
            agent_id: None,
        })
        .await?;

    // 3. Stream a chat turn
    client
        .chat(&conv.id)
        .send("Hello, llmrix!", |event| {
            if let StreamEvent::MessageChunk(e) = event {
                print!("{}", e.content);
            }
            Ok(())
        })
        .await?;

    Ok(())
}
```

---

## API Reference

### `LlmrixClient`

```rust
LlmrixClient::builder()
    .base_url("https://www.llmrix.com")   // required
    .api_key("sk-xxx")                    // optional
    .timeout(std::time::Duration::from_secs(60)) // optional, default 60 s
    .build()?
```

`LlmrixClient` is `Clone` and cheap to clone — the inner `reqwest::Client`
shares its connection pool across all clones.

---

### `ConversationsResource`

Obtained via `client.conversations()`.

| Method | Returns | Description |
|--------|---------|-------------|
| `create(req).await` | `Result<Conversation>` | Create a new conversation |
| `list(last_id, size).await` | `Result<PageResult<Conversation>>` | Cursor-paginated list |
| `get(id).await` | `Result<Conversation>` | Fetch a single conversation |
| `update(id, req).await` | `Result<Conversation>` | Update title / agent |
| `delete(id).await` | `Result<()>` | Permanently delete |
| `messages(id, last_id, size).await` | `Result<PageResult<Message>>` | Paginated message history |

Cursor pagination:

```rust
let mut page = client.conversations().list(0, 20).await?;
while page.has_more {
    let last = page.items.last().unwrap();
    page = client.conversations().list(last.seq, 20).await?;
}
```

---

### `ChatResource`

Obtained via `client.chat(conv_id)`.

| Method | Returns | Description |
|--------|---------|-------------|
| `send(message, handler).await` | `Result<()>` | Stream a plain-text message |
| `send_request(req, handler).await` | `Result<()>` | Stream a full `ChatRequest` |
| `stop().await` | `Result<()>` | Cancel the current run |
| `decide(decisions).await` | `Result<()>` | Submit HITL decisions |

#### Stream handler

```rust
type StreamHandler = impl FnMut(StreamEvent) -> Result<()>;
```

Returning `Err(...)` from the handler aborts the stream.

---

### Stream Event Types

```rust
use llmrix_sdk::streaming::event::StreamEvent;

client.chat(&conv.id).send("Search for cats", |event| {
    match event {
        StreamEvent::MessageChunk(e) => print!("{}", e.content),
        StreamEvent::ToolStart(e) => println!("\n[tool] {}", e.name),
        StreamEvent::HitlInterrupt(e) => {
            // submit decision...
        }
        StreamEvent::RunEnd(_) => {}
        StreamEvent::Error(e) => eprintln!("error: {}", e.message),
        _ => {}
    }
    Ok(())
}).await?;
```

| Variant | Channel | Description |
|---------|---------|-------------|
| `RunStart` | `lifecycle` | Agent run started |
| `RunEnd` | `lifecycle` | Agent run completed |
| `MessageChunk` | `messages` | Incremental text chunk |
| `ToolStart` | `tools` | Tool invocation started |
| `ToolEnd` | `tools` | Tool invocation finished |
| `SubagentStart` | `tools` | Sub-agent spawned |
| `SubagentEnd` | `tools` | Sub-agent finished |
| `HitlInterrupt` | `hitl` | Human approval required |
| `Error` | `error` | Run failed with error |
| `Cancelled` | `error` | Run was cancelled |
| `Heartbeat` | `heartbeat` | Keep-alive (silently dropped) |

---

### Error Handling

```rust
use llmrix_sdk::error::{LlmrixError, Result};

match client.conversations().get("bad-id").await {
    Err(LlmrixError::Api { status, body }) => {
        eprintln!("HTTP {}: {}", status, body);
    }
    Err(LlmrixError::Auth { status, body }) => {
        eprintln!("Auth failed ({}): {}", status, body);
    }
    Err(e) => eprintln!("SDK error: {}", e),
    Ok(conv) => println!("Got: {}", conv.id),
}
```

| Variant | When |
|---------|------|
| `LlmrixError::Api` | Non-2xx HTTP response; has `status` and `body` |
| `LlmrixError::Auth` | HTTP 401 / 403 |
| `LlmrixError::Http` | Network-level `reqwest` error |
| `LlmrixError::Sse` | Unrecoverable SSE parse error |

---

## Advanced Usage

### HITL (Human-in-the-Loop)

```rust
use llmrix_sdk::model::HitlDecision;

let chat = client.chat(&conv.id);
chat.send("Delete old backups", |event| {
    if let StreamEvent::HitlInterrupt(e) = event {
        println!("Approve '{}'? (y/n)", e.action_name);
        let mut ans = String::new();
        std::io::stdin().read_line(&mut ans).unwrap();
        let decision = if ans.trim() == "y" {
            HitlDecision::approve()
        } else {
            HitlDecision::reject("user declined")
        };
        // Note: use tokio::task::block_in_place or restructure as needed
        // for true async decide
        println!("Decision recorded: {:?}", decision);
    }
    Ok(())
}).await?;
```

### Collecting the full response

```rust
async fn run_to_completion(
    client: &LlmrixClient,
    conv_id: &str,
    message: &str,
) -> Result<String> {
    let mut output = String::new();
    client.chat(conv_id).send(message, |event| {
        if let StreamEvent::MessageChunk(e) = event {
            output.push_str(&e.content);
        }
        Ok(())
    }).await?;
    Ok(output)
}
```

---

## Building from Source

```bash
git clone https://github.com/llmrix/llmrix-rust-sdk.git
cd llmrix-rust-sdk
cargo build
cargo test
```

Generate documentation:

```bash
cargo doc --open
```

---

## Contributing

Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) first.

---

## License

Apache License 2.0 — see the [LICENSE](LICENSE) file.