diff --git a/runagent-rust/PUBLISH.md b/runagent-rust/PUBLISH.md new file mode 100644 index 0000000..e0bb5c5 --- /dev/null +++ b/runagent-rust/PUBLISH.md @@ -0,0 +1,49 @@ +# Publishing the Rust SDK (`runagent-rust/runagent`) + +Follow this checklist whenever you cut a new release of the Rust SDK. + +## 1. Prerequisites + +- Cargo credentials with publish rights to `runagent`. +- `cargo login ` already configured on the machine/CI runner. +- Clean git tree (all changes committed). + +## 2. Version Bump + +1. Update `version` in `runagent-rust/runagent/Cargo.toml`. +2. Update `runagent-rust/Cargo.toml` (workspace) if needed. +3. Update the version badge/examples in `runagent-rust/runagent/README.md`. + +## 3. Build & Test + +```bash +cd runagent-rust/runagent +cargo fmt +cargo clippy --all-targets --all-features -- -D warnings +cargo test --all-features +``` + +Optional: exercise key examples (local + remote) before publishing. + +## 4. Package Audit + +```bash +cargo package +``` + +Inspect `target/package/runagent-*.crate` (or run `cargo package --list`) to ensure only the expected files are included. + +## 5. Publish + +```bash +cargo publish +``` + +If 2FA is enabled, be ready to provide the OTP. + +## 6. Post-Publish + +- Tag the release: `git tag runagent-rust-vX.Y.Z && git push origin runagent-rust-vX.Y.Z`. +- Update release notes / changelog as needed. +- Ensure docs (README, SDK checklist) reflect any new behavior. + diff --git a/runagent-rust/examples/async_example.rs b/runagent-rust/examples/async_example.rs new file mode 100644 index 0000000..057a2a7 --- /dev/null +++ b/runagent-rust/examples/async_example.rs @@ -0,0 +1,27 @@ +//! Async example using RunAgentClient +//! +//! This is the recommended approach for most use cases. + +use runagent::{RunAgentClient, RunAgentClientConfig}; +use serde_json::json; + +#[tokio::main] +async fn main() -> runagent::RunAgentResult<()> { + // dotenvy::from_filename("local.env").ok(); // optional + + let agent_id = "a6977384-6c88-40dc-a629-e6bf077786ae"; + let entrypoint_tag = "minimal"; + + let client = RunAgentClient::new( + RunAgentClientConfig::new(agent_id, entrypoint_tag) + .with_local(true) + .with_address("127.0.0.1", 8452) + .with_enable_registry(false) // Skip DB lookup since we have explicit address + ).await?; + + let response = client.run(&[("message", json!("Hello!"))]).await?; + + println!("Response: {}", response); + Ok(()) +} + diff --git a/runagent-rust/examples/direct_construction.rs b/runagent-rust/examples/direct_construction.rs new file mode 100644 index 0000000..1ae0a10 --- /dev/null +++ b/runagent-rust/examples/direct_construction.rs @@ -0,0 +1,28 @@ +//! Example showing direct struct construction (TypeScript-like interface) +//! +//! This matches the TypeScript SDK pattern where you pass a config object directly. + +use runagent::RunAgentClient; +use serde_json::json; + +#[tokio::main] +async fn main() -> runagent::RunAgentResult<()> { + // Direct struct construction - matches TypeScript interface + let client = RunAgentClient::new(runagent::RunAgentClientConfig { + agent_id: "a6977384-6c88-40dc-a629-e6bf077786ae".to_string(), + entrypoint_tag: "minimal".to_string(), + api_key: Some("rau_b4dcebdef6386726b08971a1cc968d8a2b77c5834d30f3f5a43bddf065cd95cb".to_string()), + base_url: Some("http://localhost:8333/".to_string()), + local: None, + host: None, + port: None, + extra_params: None, + enable_registry: None, + }).await?; + + let response = client.run(&[("message", json!("Hello!"))]).await?; + + println!("Response: {}", response); + Ok(()) +} + diff --git a/runagent-rust/examples/sync_example.rs b/runagent-rust/examples/sync_example.rs new file mode 100644 index 0000000..e288a49 --- /dev/null +++ b/runagent-rust/examples/sync_example.rs @@ -0,0 +1,27 @@ +//! Sync example using blocking RunAgentClient +//! +//! This is useful for simple scripts or when you can't use async/await. +//! Note: For better performance, prefer the async version. + +use runagent::blocking::{RunAgentClient, RunAgentClientConfig}; +use serde_json::json; + +fn main() -> runagent::RunAgentResult<()> { + // dotenvy::from_filename("local.env").ok(); // optional + + let agent_id = "a6977384-6c88-40dc-a629-e6bf077786ae"; + let entrypoint_tag = "minimal"; + + let client = RunAgentClient::new( + RunAgentClientConfig::new(agent_id, entrypoint_tag) + .with_local(true) + .with_address("127.0.0.1", 8452) + .with_enable_registry(false) // Skip DB lookup since we have explicit address + )?; + + let response = client.run(&[("message", json!("Hello!"))])?; + + println!("Response: {}", response); + Ok(()) +} + diff --git a/runagent-rust/runagent/README.md b/runagent-rust/runagent/README.md index 5366304..bfca33a 100644 --- a/runagent-rust/runagent/README.md +++ b/runagent-rust/runagent/README.md @@ -1,41 +1,22 @@ # RunAgent Rust SDK [![Crates.io](https://img.shields.io/crates/v/runagent.svg)](https://crates.io/crates/runagent) -[![Documentation](https://docs.rs/runagent/badge.svg)](https://docs.rs/runagent) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Docs](https://docs.rs/runagent/badge.svg)](https://docs.rs/runagent) [![Build Status](https://github.com/runagent-dev/runagent/workflows/CI/badge.svg)](https://github.com/runagent-dev/runagent/actions) ---- - -## What is RunAgent? - -RunAgent is a comprehensive Rust SDK for deploying and managing AI agents with support for multiple frameworks including **LangChain**, **LangGraph**, **LlamaIndex**, and more. Whether you're building chatbots, autonomous agents, or complex AI workflows, RunAgent provides the tools you need to deploy, test, and scale your AI applications. - ---- - -## Features - -- **Multi-Framework Support**: LangChain, LangGraph, LlamaIndex, Letta, CrewAI, AutoGen -- **Local & Remote Deployment**: Deploy agents locally or to remote servers -- **Real-time Streaming**: WebSocket-based streaming for real-time interactions -- **Database Management**: SQLite-based agent metadata and history -- **Template System**: Pre-built templates for rapid setup -- **Type Safety**: Full Rust type safety with error handling -- **Async/Await**: Powered by Tokio for async operations +Rust bindings for the RunAgent platform. Use it to call agents you deploy with the CLI—whether they run locally on your laptop or remotely on `backend.run-agent.ai`. --- ## Installation ```bash -cargo add runagent tokio +cargo add runagent tokio serde_json futures ``` -Or add manually to `Cargo.toml`: - ```toml [dependencies] -runagent = "0.1.0" +runagent = "0.1" tokio = { version = "1.35", features = ["full"] } serde_json = "1.0" futures = "0.3" @@ -43,390 +24,293 @@ futures = "0.3" --- -## Quick Start - -> **RunAgent Cloud** is the recommended way to get started. Deploy and interact with agents hosted on RunAgent's infrastructure without managing your own servers. - -### RunAgent Cloud +## Configuration Overview -RunAgent Cloud allows you to deploy and interact with agents hosted on RunAgent's infrastructure. This is the recommended way to get started quickly. +The SDK uses a single constructor pattern. All configuration is done through `RunAgentClientConfig`: -**Key Benefits:** -- No server setup required -- Automatic scaling -- Managed infrastructure -- Simple authentication via API key - -#### Step 1: Set Up Authentication +```rust +use runagent::RunAgentClientConfig; -**Important:** You must export your API key before running your application: +// Local agent with explicit address +let client = RunAgentClient::new( + RunAgentClientConfig::new("agent-id", "entrypoint") + .with_local(true) + .with_address("127.0.0.1", 8450) + .with_enable_registry(false) +).await?; -```bash -export RUNAGENT_API_KEY="your-api-key" +// Remote agent +let client = RunAgentClient::new( + RunAgentClientConfig::new("agent-id", "entrypoint") + .with_api_key(env::var("RUNAGENT_API_KEY").unwrap()) +).await?; ``` -You can get your API key from the [RunAgent Dashboard](https://runagent.dev). +| Setting | Cloud | Local (auto discovery) | Local (explicit) | +|-----------------|------------------|------------------------|-------------------| +| `local` | `false` (default) | `true` | `true` | +| Host / Port | derived from URL | looked up via SQLite | `with_address()` | +| Base URL | `RUNAGENT_BASE_URL` \|\| default | n/a | n/a | +| API Key | `RUNAGENT_API_KEY` (required) | optional | optional | +| Registry | n/a | `true` (default) | `false` | -#### Step 2: Connect to Your Agent +- `RUNAGENT_API_KEY`: Bearer token for remote agents (can be set via env var or `with_api_key()`). +- `RUNAGENT_BASE_URL`: Override the default cloud endpoint (e.g. staging). +- For local discovery, install the crate with the `db` feature and ensure the CLI has registered the agent in `~/.runagent/runagent_local.db`. -When connecting to RunAgent Cloud, set `local = false`: +--- -```rust -use runagent::client::RunAgentClient; +## Usage -let client = RunAgentClient::new( - "your-agent-id", // Your agent ID from RunAgent Cloud - "agno_print_response", // Entrypoint tag - false // local = false for cloud -).await?; -``` - -#### Step 3: Run Your Agent +### Sync (Blocking) - Simplest -**Non-Streaming Example:** +#### Non-streaming ```rust -use runagent::client::RunAgentClient; +use runagent::blocking::{RunAgentClient, RunAgentClientConfig}; use serde_json::json; -#[tokio::main] -async fn main() -> Result<(), Box> { - // Set RUNAGENT_API_KEY environment variable before running - let agent_id = "your-agent-id"; - - // Connect to cloud agent (local = false) - let client = RunAgentClient::new(agent_id, "agno_print_response", false).await?; - - // Run with positional and keyword arguments - let response = client.run_with_args( - &[json!("Write small paragraph on how i met your mother tv series")], // positional args - &[] // no keyword args - ).await?; - +fn main() -> runagent::RunAgentResult<()> { + // Direct struct construction + let client = RunAgentClient::new(RunAgentClientConfig { + agent_id: "agent-id".to_string(), + entrypoint_tag: "entrypoint".to_string(), + api_key: Some("your-api-key".to_string()), + base_url: Some("http://localhost:8333/".to_string()), + ..RunAgentClientConfig::default() // Omits None values + })?; + + let response = client.run(&[("message", json!("Hello!"))])?; println!("Response: {}", response); Ok(()) } ``` -**Streaming Example:** +#### Streaming ```rust -use runagent::client::RunAgentClient; +use runagent::blocking::{RunAgentClient, RunAgentClientConfig}; use serde_json::json; -use futures::StreamExt; -#[tokio::main] -async fn main() -> Result<(), Box> { - // Set RUNAGENT_API_KEY environment variable before running - let agent_id = "your-agent-id"; - - // Connect to cloud agent with streaming entrypoint - let client = RunAgentClient::new(agent_id, "agno_print_response_stream", false).await?; - - // Run with streaming - let mut stream = client.run_stream(&[ - ("prompt", json!("is investing in AI is good idea?")) - ]).await?; - - while let Some(chunk_result) = stream.next().await { - match chunk_result { - Ok(chunk) => println!("{}", chunk), - Err(e) => { - println!("Error: {}", e); - break; - } - } +fn main() -> runagent::RunAgentResult<()> { + let client = RunAgentClient::new(RunAgentClientConfig { + agent_id: "agent-id".to_string(), + entrypoint_tag: "entrypoint_stream".to_string(), + api_key: Some("your-api-key".to_string()), + ..RunAgentClientConfig::default() + })?; + + // Streaming collects all chunks into a vector + let chunks = client.run_stream(&[("message", json!("Hello!"))])?; + for chunk in chunks { + println!(">> {}", chunk?); } - Ok(()) } ``` -**Complete Workflow:** +### Async (Recommended) -```bash -# 1. Export your API key -export RUNAGENT_API_KEY="your-api-key" - -# 2. Run your application -cargo run -``` - ---- - -### Local Development - -For local development, you can run agents on your own machine. Set `local = true` when creating the client. - -#### Basic Agent Interaction (Local) +#### Non-streaming ```rust -use runagent::client::RunAgentClient; +use runagent::{RunAgentClient, RunAgentClientConfig}; use serde_json::json; #[tokio::main] -async fn main() -> Result<(), Box> { - let agent_id = "your-agent-id"; - - // Connect to local agent (local = true) - let client = RunAgentClient::new(agent_id, "lead_score_flow", true).await?; - - // Run with keyword arguments only - let response = client.run_with_args( - &[], // no positional args - &[ - ("top_n", json!(1)), - ("generate_emails", json!(true)) - ] - ).await?; - - println!("Response: {}", serde_json::to_string_pretty(&response)?); +async fn main() -> runagent::RunAgentResult<()> { + // Direct struct construction + let client = RunAgentClient::new(RunAgentClientConfig { + agent_id: "agent-id".to_string(), + entrypoint_tag: "entrypoint".to_string(), + api_key: Some("your-api-key".to_string()), + base_url: Some("http://localhost:8333/".to_string()), + ..RunAgentClientConfig::default() + }).await?; + + let response = client.run(&[("message", json!("Hello!"))]).await?; + println!("Response: {}", response); Ok(()) } ``` -### Connecting to Local Agent with Explicit Address +#### Streaming ```rust -use runagent::client::RunAgentClient; +use runagent::{RunAgentClient, RunAgentClientConfig}; use serde_json::json; +use futures::StreamExt; #[tokio::main] -async fn main() -> Result<(), Box> { - let agent_id = "your-agent-id"; - - // Connect to local agent with explicit host and port - let client = RunAgentClient::with_address( - agent_id, - "generic", - true, - Some("127.0.0.1"), - Some(8452) - ).await?; - - let response = client.run(&[ - ("message", json!("Hello, world!")) - ]).await?; - - println!("Response: {}", response); +async fn main() -> runagent::RunAgentResult<()> { + let client = RunAgentClient::new(RunAgentClientConfig { + agent_id: "agent-id".to_string(), + entrypoint_tag: "entrypoint_stream".to_string(), + api_key: Some("your-api-key".to_string()), + ..RunAgentClientConfig::default() + }).await?; + + // Real streaming - processes chunks as they arrive + let mut stream = client.run_stream(&[("message", json!("Hello!"))]).await?; + while let Some(chunk) = stream.next().await { + println!(">> {}", chunk?); + } Ok(()) } ``` ---- - -## Configuration - -### RunAgent Cloud Setup +### Alternative: Builder Pattern -**Required:** Set your API key as an environment variable before running your application: +You can also use the builder pattern instead of direct struct construction: -```bash -export RUNAGENT_API_KEY="your-api-key" -``` +```rust +use runagent::{RunAgentClient, RunAgentClientConfig}; -**Optional:** Customize the base URL (defaults to `https://api.runagent.ai`): +// Async +let client = RunAgentClient::new( + RunAgentClientConfig::new("agent-id", "entrypoint") + .with_api_key("your-api-key") + .with_base_url("http://localhost:8333/") +).await?; -```bash -export RUNAGENT_BASE_URL="https://api.runagent.ai" +// Sync +use runagent::blocking::RunAgentClient; +let client = RunAgentClient::new( + RunAgentClientConfig::new("agent-id", "entrypoint") + .with_api_key("your-api-key") + .with_base_url("http://localhost:8333/") +)?; ``` -### Local Development Setup +### Local Agents -For local development, you can configure cache and logging: +#### With explicit address -```bash -export RUNAGENT_CACHE_DIR="~/.runagent" -export RUNAGENT_LOGGING_LEVEL="info" +```rust +use runagent::{RunAgentClient, RunAgentClientConfig}; + +let client = RunAgentClient::new(RunAgentClientConfig { + agent_id: "local-agent-id".to_string(), + entrypoint_tag: "minimal".to_string(), + local: Some(true), + host: Some("127.0.0.1".to_string()), + port: Some(8452), + enable_registry: Some(false), // Skip DB lookup + ..RunAgentClientConfig::default() +}).await?; ``` -### Quick Reference - -| Setting | RunAgent Cloud | Local Development | -|---------|---------------|-------------------| -| **API Key** | **Required** (`RUNAGENT_API_KEY`) | Not needed | -| **Base URL** | Optional (defaults to `https://api.runagent.ai`) | Not needed | -| **Client Parameter** | `local = false` | `local = true` | -| **Agent Location** | RunAgent infrastructure | Your local machine | - -### Configuration Builder - -You can also configure the SDK programmatically: +#### With auto-discovery (requires `db` feature) ```rust -use runagent::RunAgentConfig; - -let config = RunAgentConfig::new() - .with_api_key("your-api-key") - .with_base_url("https://api.runagent.ai") - .with_logging() - .build(); +let client = RunAgentClient::new(RunAgentClientConfig { + agent_id: "local-agent-id".to_string(), + entrypoint_tag: "minimal".to_string(), + local: Some(true), + // enable_registry defaults to true for local agents + ..RunAgentClientConfig::default() +}).await?; ``` ---- - -## Architecture - -### Core Components - -* **Client**: High-level client for agent interaction -* **REST Client**: HTTP-based client for non-streaming requests -* **Socket Client**: WebSocket-based client for streaming interactions -* **Database**: SQLite-based agent history store (optional) -* **Serialization**: Safe messaging via WebSocket +> **Guardrails**: tags ending with `_stream` can only be run via `run_stream*`. Non-stream tags must be run via `run*`. The client raises clear errors (`STREAM_ENTRYPOINT`, `NON_STREAM_ENTRYPOINT`) with suggestions. -### Optional Features - -Enable or disable features in `Cargo.toml`: +--- -```toml -[dependencies] -runagent = { version = "0.1.0", features = ["db"] } +## Architecture Expectations + +During initialization the client calls `/api/v1/agents/{id}/architecture` and expects the envelope: + +```json +{ + "success": true, + "data": { + "agent_id": "…", + "entrypoints": [ + { "tag": "minimal", "file": "main.py", "module": "run", "extractor": {} } + ] + }, + "message": "Agent architecture retrieved successfully", + "error": null, + "timestamp": "…", + "request_id": "…" +} ``` -Available features: - -* `db` (default): Enable database support for local agent management +- If `success === false` we propagate `error.code/message/suggestion/details`. +- If `data.entrypoints` is missing we raise `ARCHITECTURE_MISSING`. +- When an entrypoint can’t be found we log the list returned by the server to help debug typos. --- ## API Reference -### `RunAgentClient` +### Client Creation -Main client for interacting with RunAgent deployments. +| Method | Description | +|--------|-------------| +| `RunAgentClient::new(config: RunAgentClientConfig)` | Single constructor for all client types. | -#### Methods - -* `new(agent_id, entrypoint_tag, local)` - Create a new client - * `agent_id`: The agent identifier - * `entrypoint_tag`: The entrypoint function tag (e.g., "agno_print_response") - * `local`: `true` for local agents, `false` for cloud agents - -* `with_address(agent_id, entrypoint_tag, local, host, port)` - Create client with explicit address - -* `run(input_kwargs)` - Run agent with keyword arguments only - * Returns: `RunAgentResult` - -* `run_with_args(input_args, input_kwargs)` - Run agent with both positional and keyword arguments - * `input_args`: Slice of positional arguments as `Value` - * `input_kwargs`: Slice of tuples `(&str, Value)` for keyword arguments - -* `run_stream(input_kwargs)` - Run agent with streaming response - * Returns: `RunAgentResult> + Send>>>` - -* `run_stream_with_args(input_args, input_kwargs)` - Run agent with streaming and both argument types - -* `health_check()` - Check if the agent is available - -* `get_agent_architecture()` - Get the agent's architecture information - -### `DatabaseService` - -Database service for managing local agent metadata (requires `db` feature). +### Configuration Builder -* `new(db_path)` - Create a new database service -* `add_agent(agent)` - Add an agent to the database -* `list_agents()` - List all agents in the database -* `get_agent(agent_id)` - Get agent information by ID +| Method | Description | +|--------|-------------| +| `RunAgentClientConfig::new(agent_id, entrypoint_tag)` | Create config with required fields. | +| `.with_local(bool)` | Set local flag (default: `false`). | +| `.with_address(host, port)` | Set explicit host/port for local agents. | +| `.with_api_key(key)` | Set API key (overrides env var). | +| `.with_base_url(url)` | Override default base URL. | +| `.with_enable_registry(bool)` | Enable/disable database lookup (default: `true` for local). | +| `.with_extra_params(params)` | Set extra parameters for future use. | ---- +### Client Methods -## Error Handling +| Method | Description | +|--------|-------------| +| `run` / `run_with_args` | Execute non-streaming entrypoints. | +| `run_stream` / `run_stream_with_args` | Execute streaming entrypoints (async stream of `Value`). | +| `health_check` | Check if the agent is reachable. | +| `get_agent_architecture` | Fetch the normalized architecture (see above). | -```rust -use runagent::{RunAgentError, RunAgentResult}; - -fn handle_errors() -> RunAgentResult<()> { - match some_operation() { - Ok(result) => Ok(result), - Err(RunAgentError::Authentication { message }) => { - eprintln!("Auth error: {}", message); - Err(RunAgentError::authentication("Invalid credentials")) - } - Err(RunAgentError::Connection { message }) => { - eprintln!("Connection error: {}", message); - Err(RunAgentError::connection("Connection failed")) - } - Err(e) => Err(e), - } -} -``` +All methods return `RunAgentResult` where `RunAgentError::Execution { code, message, suggestion, details }` carries actionable metadata (e.g. `AGENT_NOT_FOUND_REMOTE`, `STREAM_ENTRYPOINT`, `AUTHENTICATION_ERROR`). Inspect these fields to guide users. --- -## Testing +## Troubleshooting -```bash -cargo test -cargo test --all-features -cargo test --test integration -``` +| Symptom | Resolution | +|---------|------------| +| `STREAM_ENTRYPOINT` | Call `run_stream*` or switch to a non-stream tag. | +| `NON_STREAM_ENTRYPOINT` | Call `run*` or deploy a `_stream` entrypoint. | +| `AGENT_NOT_FOUND_LOCAL` | Ensure the agent is registered locally (`runagent serve` or `runagent config --register-agent`). | +| `AGENT_NOT_FOUND_REMOTE` | Verify the agent ID and that your API key has access. | +| `AUTHENTICATION_ERROR` | Set `RUNAGENT_API_KEY` env var or use `.with_api_key()` in config. | +| `ARCHITECTURE_MISSING` | Redeploy the agent; ensure entrypoints are defined in `runagent.config.json`. | --- -## Examples - -See the `examples/` folder for complete examples: +## Security Best Practices -* Basic usage with cloud agents -* Streaming interactions -* Local agent connections -* Framework integrations +- Never hardcode API keys; use env vars or secret managers. +- For browser/edge bridging, proxy calls through your backend rather than exposing long-lived keys. +- When running locally, restrict access to `~/.runagent/runagent_local.db`. --- -## Contributing - -We welcome contributions! See `CONTRIBUTING.md` for guidelines. - -### Development Setup +## Development & Publishing ```bash -git clone https://github.com/runagent-dev/runagent.git -cd runagent/runagent-rust -cargo build -cargo test +cargo fmt +cargo clippy --all-targets --all-features -- -D warnings +cargo test --all-features ``` ---- - -## Roadmap - -* Python interop via PyO3 -* Additional framework support -* Enhanced streaming capabilities -* Production deployment tools -* Monitoring & observability -* CLI tool integration +To publish a new release, follow [`../PUBLISH.md`](../PUBLISH.md) (version bump, `cargo package`, `cargo publish`, tag release). --- -## Links - -* [Website](https://run-agent.ai/) -* [Documentation](https://docs.run-agent.ai/explanation/introduction) -* [Repository](https://github.com/runagent-dev/runagent) -* [Issues](https://github.com/runagent-dev/runagent/issues) -* [Python SDK](https://pypi.org/project/runagent/) - ---- - -## License - -This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file. - ---- - -## Acknowledgments - -* Built with [Tokio](https://tokio.rs) -* Uses [Axum](https://github.com/tokio-rs/axum) -* SQL powered by [SQLx](https://github.com/launchbadge/sqlx) -* WebSocket support via [tokio-tungstenite](https://github.com/snapview/tokio-tungstenite) - ---- - -Need help? Join our [Discord](https://discord.gg/runagent) or check the documentation! - +## Need Help? +- Docs: `docs/sdk/rust/` (coming soon) or https://docs.rs/runagent +- Issues: [github.com/runagent-dev/runagent/issues](https://github.com/runagent-dev/runagent/issues) +- Community: Discord link in the main repo README +- Commercial support: contact the RunAgent team via the dashboard diff --git a/runagent-rust/runagent/examples/async_example.rs b/runagent-rust/runagent/examples/async_example.rs new file mode 100644 index 0000000..057a2a7 --- /dev/null +++ b/runagent-rust/runagent/examples/async_example.rs @@ -0,0 +1,27 @@ +//! Async example using RunAgentClient +//! +//! This is the recommended approach for most use cases. + +use runagent::{RunAgentClient, RunAgentClientConfig}; +use serde_json::json; + +#[tokio::main] +async fn main() -> runagent::RunAgentResult<()> { + // dotenvy::from_filename("local.env").ok(); // optional + + let agent_id = "a6977384-6c88-40dc-a629-e6bf077786ae"; + let entrypoint_tag = "minimal"; + + let client = RunAgentClient::new( + RunAgentClientConfig::new(agent_id, entrypoint_tag) + .with_local(true) + .with_address("127.0.0.1", 8452) + .with_enable_registry(false) // Skip DB lookup since we have explicit address + ).await?; + + let response = client.run(&[("message", json!("Hello!"))]).await?; + + println!("Response: {}", response); + Ok(()) +} + diff --git a/runagent-rust/runagent/examples/direct_construction.rs b/runagent-rust/runagent/examples/direct_construction.rs new file mode 100644 index 0000000..5f8aaf7 --- /dev/null +++ b/runagent-rust/runagent/examples/direct_construction.rs @@ -0,0 +1,28 @@ +//! Example showing direct struct construction +//! +//! This shows how to pass a config object directly to the client constructor. + +use runagent::RunAgentClient; +use serde_json::json; + +#[tokio::main] +async fn main() -> runagent::RunAgentResult<()> { + // Direct struct construction + let client = RunAgentClient::new(runagent::RunAgentClientConfig { + agent_id: "a6977384-6c88-40dc-a629-e6bf077786ae".to_string(), + entrypoint_tag: "minimal".to_string(), + api_key: Some("rau_b4dcebdef6386726b08971a1cc968d8a2b77c5834d30f3f5a43bddf065cd95cb".to_string()), + base_url: Some("http://localhost:8333/".to_string()), + local: None, + host: None, + port: None, + extra_params: None, + enable_registry: None, + }).await?; + + let response = client.run(&[("message", json!("Hello!"))]).await?; + + println!("Response: {}", response); + Ok(()) +} + diff --git a/runagent-rust/runagent/examples/direct_construction_omitted.rs b/runagent-rust/runagent/examples/direct_construction_omitted.rs new file mode 100644 index 0000000..0587f26 --- /dev/null +++ b/runagent-rust/runagent/examples/direct_construction_omitted.rs @@ -0,0 +1,24 @@ +//! Example showing direct struct construction with omitted None values +//! +//! Using `..Default::default()` or `..RunAgentClientConfig::default()` to omit None fields. + +use runagent::RunAgentClient; +use serde_json::json; + +#[tokio::main] +async fn main() -> runagent::RunAgentResult<()> { + // You can omit None values using .. syntax + let client = RunAgentClient::new(runagent::RunAgentClientConfig { + agent_id: "a6977384-6c88-40dc-a629-e6bf077786ae".to_string(), + entrypoint_tag: "minimal".to_string(), + api_key: Some("rau_b4dcebdef6386726b08971a1cc968d8a2b77c5834d30f3f5a43bddf065cd95cb".to_string()), + base_url: Some("http://localhost:8333/".to_string()), + ..runagent::RunAgentClientConfig::default() // Omits all None fields + }).await?; + + let response = client.run(&[("message", json!("Hello!"))]).await?; + + println!("Response: {}", response); + Ok(()) +} + diff --git a/runagent-rust/runagent/examples/sync_example.rs b/runagent-rust/runagent/examples/sync_example.rs new file mode 100644 index 0000000..e288a49 --- /dev/null +++ b/runagent-rust/runagent/examples/sync_example.rs @@ -0,0 +1,27 @@ +//! Sync example using blocking RunAgentClient +//! +//! This is useful for simple scripts or when you can't use async/await. +//! Note: For better performance, prefer the async version. + +use runagent::blocking::{RunAgentClient, RunAgentClientConfig}; +use serde_json::json; + +fn main() -> runagent::RunAgentResult<()> { + // dotenvy::from_filename("local.env").ok(); // optional + + let agent_id = "a6977384-6c88-40dc-a629-e6bf077786ae"; + let entrypoint_tag = "minimal"; + + let client = RunAgentClient::new( + RunAgentClientConfig::new(agent_id, entrypoint_tag) + .with_local(true) + .with_address("127.0.0.1", 8452) + .with_enable_registry(false) // Skip DB lookup since we have explicit address + )?; + + let response = client.run(&[("message", json!("Hello!"))])?; + + println!("Response: {}", response); + Ok(()) +} + diff --git a/runagent-rust/runagent/examples/test_deserialize.rs b/runagent-rust/runagent/examples/test_deserialize.rs new file mode 100644 index 0000000..a7c584c --- /dev/null +++ b/runagent-rust/runagent/examples/test_deserialize.rs @@ -0,0 +1,17 @@ +use runagent::utils::serializer::CoreSerializer; +use serde_json::json; + +fn main() { + let serializer = CoreSerializer::new(10.0).unwrap(); + + // Test the exact structure we're getting + let test_response = json!({ + "payload": "\"Hello, world!\"", + "type": "string" + }); + + println!("Input: {}", test_response); + let result = serializer.deserialize_object(test_response).unwrap(); + println!("Result: {}", result); + println!("Result is string: {}", result.is_string()); +} diff --git a/runagent-rust/runagent/src/blocking.rs b/runagent-rust/runagent/src/blocking.rs new file mode 100644 index 0000000..c9eb5e1 --- /dev/null +++ b/runagent-rust/runagent/src/blocking.rs @@ -0,0 +1,145 @@ +//! Blocking (synchronous) wrapper for RunAgentClient +//! +//! This module provides a synchronous interface that wraps the async client. +//! It uses a Tokio runtime internally to block on async operations. +//! +//! # Example +//! +//! ```rust,no_run +//! use runagent::blocking::{RunAgentClient, RunAgentClientConfig}; +//! use serde_json::json; +//! +//! fn main() -> runagent::RunAgentResult<()> { +//! let client = RunAgentClient::new( +//! RunAgentClientConfig::new("agent-id", "entrypoint") +//! .with_api_key(Some("key".to_string())) +//! )?; +//! +//! let result = client.run(&[("message", json!("Hello"))])?; +//! println!("Result: {}", result); +//! Ok(()) +//! } +//! ``` + +use crate::client::RunAgentClient as AsyncRunAgentClient; +use crate::types::{RunAgentError, RunAgentResult}; +use serde_json::Value; +use std::collections::HashMap; +use tokio::runtime::Runtime; + +// Re-export for convenience +pub use crate::client::RunAgentClientConfig; + +/// Blocking (synchronous) wrapper for RunAgentClient +/// +/// This client wraps the async client and blocks on async operations. +/// It's useful for simple scripts or when you can't use async/await. +/// +/// Note: For better performance and resource usage, prefer the async client. +pub struct RunAgentClient { + inner: AsyncRunAgentClient, + runtime: Runtime, +} + +impl RunAgentClient { + /// Create a new blocking RunAgent client + /// + /// This will create a Tokio runtime internally and block until the client is initialized. + pub fn new(config: RunAgentClientConfig) -> RunAgentResult { + let runtime = Runtime::new() + .map_err(|e| RunAgentError::connection(format!("Failed to create runtime: {}", e)))?; + + let inner = runtime.block_on(AsyncRunAgentClient::new(config))?; + + Ok(Self { inner, runtime }) + } + + /// Execute a non-streaming entrypoint + /// + /// This blocks until the agent execution completes. + pub fn run(&self, input_kwargs: &[(&str, Value)]) -> RunAgentResult { + self.runtime.block_on(self.inner.run(input_kwargs)) + } + + /// Execute a non-streaming entrypoint with both args and kwargs + pub fn run_with_args( + &self, + input_args: &[Value], + input_kwargs: &[(&str, Value)], + ) -> RunAgentResult { + self.runtime + .block_on(self.inner.run_with_args(input_args, input_kwargs)) + } + + /// Execute a streaming entrypoint + /// + /// Returns a vector of all chunks collected from the stream. + /// Note: This collects the entire stream, so it's not truly streaming. + /// For real streaming, use the async client. + pub fn run_stream( + &self, + input_kwargs: &[(&str, Value)], + ) -> RunAgentResult>> { + let stream = self.runtime.block_on(self.inner.run_stream(input_kwargs))?; + Ok(self.runtime.block_on(async { + use futures::StreamExt; + let mut results = Vec::new(); + let mut stream = stream; + while let Some(item) = stream.next().await { + results.push(item); + } + results + })) + } + + /// Execute a streaming entrypoint with both args and kwargs + pub fn run_stream_with_args( + &self, + input_args: &[Value], + input_kwargs: &[(&str, Value)], + ) -> RunAgentResult>> { + let stream = self + .runtime + .block_on(self.inner.run_stream_with_args(input_args, input_kwargs))?; + Ok(self.runtime.block_on(async { + use futures::StreamExt; + let mut results = Vec::new(); + let mut stream = stream; + while let Some(item) = stream.next().await { + results.push(item); + } + results + })) + } + + /// Get agent architecture + pub fn get_agent_architecture(&self) -> RunAgentResult { + self.runtime.block_on(self.inner.get_agent_architecture()) + } + + /// Health check + pub fn health_check(&self) -> RunAgentResult { + self.runtime.block_on(self.inner.health_check()) + } + + /// Get agent ID + pub fn agent_id(&self) -> &str { + self.inner.agent_id() + } + + /// Get entrypoint tag + pub fn entrypoint_tag(&self) -> &str { + self.inner.entrypoint_tag() + } + + /// Get extra parameters + pub fn extra_params(&self) -> Option<&HashMap> { + self.inner.extra_params() + } + + /// Check if this is a local client + pub fn is_local(&self) -> bool { + self.inner.is_local() + } +} + diff --git a/runagent-rust/runagent/src/client/mod.rs b/runagent-rust/runagent/src/client/mod.rs index e49937d..612b88f 100644 --- a/runagent-rust/runagent/src/client/mod.rs +++ b/runagent-rust/runagent/src/client/mod.rs @@ -5,6 +5,6 @@ pub mod runagent_client; pub mod socket_client; // Re-export the main client -pub use runagent_client::RunAgentClient; +pub use runagent_client::{RunAgentClient, RunAgentClientConfig}; pub use rest_client::RestClient; pub use socket_client::SocketClient; \ No newline at end of file diff --git a/runagent-rust/runagent/src/client/rest_client.rs b/runagent-rust/runagent/src/client/rest_client.rs index fc2eaba..05d60db 100644 --- a/runagent-rust/runagent/src/client/rest_client.rs +++ b/runagent-rust/runagent/src/client/rest_client.rs @@ -220,50 +220,62 @@ impl RestClient { /// Get agent architecture information pub async fn get_agent_architecture(&self, agent_id: &str) -> RunAgentResult { let path = format!("agents/{}/architecture", agent_id); - self.get(&path).await - .and_then(|response| { - if let Some(success) = response.get("success").and_then(|v| v.as_bool()) { - if success { - if let Some(data) = response.get("data") { - return Ok(data.clone()); - } - return Err(RunAgentError::server( - "Architecture response missing data".to_string(), - )); - } + let response = self.get(&path).await?; - let message = response - .get("error") - .and_then(|err| { - if err.is_object() { - err.get("message").and_then(|m| m.as_str()).map(|s| s.to_string()) - } else { - err.as_str().map(|s| s.to_string()) - } - }) - .or_else(|| { - response - .get("message") - .and_then(|m| m.as_str()) - .map(|s| s.to_string()) - }) - .unwrap_or_else(|| "Failed to retrieve agent architecture".to_string()); - - return Err(RunAgentError::server(message)); + if let Some(success) = response.get("success").and_then(|v| v.as_bool()) { + if success { + if let Some(data) = response.get("data") { + return Ok(data.clone()); } + return Err(RunAgentError::execution( + "ARCHITECTURE_MISSING", + "Architecture response missing data", + Some("Redeploy the agent or ensure entrypoints are configured.".to_string()), + Some(response), + )); + } - Ok(response) - }) - .map_err(|e| { - if e.category() == "validation" && e.to_string().contains("Not found") { - RunAgentError::validation(format!( - "Agent {} not found on server. Check that the agent exists and is deployed. Error: {}", - agent_id, e - )) + let (code, message, suggestion) = if let Some(error_obj) = response.get("error") { + if let Some(obj) = error_obj.as_object() { + ( + obj.get("code") + .and_then(|c| c.as_str()) + .unwrap_or("UNKNOWN_ERROR") + .to_string(), + obj.get("message") + .and_then(|m| m.as_str()) + .unwrap_or("Failed to retrieve agent architecture") + .to_string(), + obj.get("suggestion") + .and_then(|s| s.as_str()) + .map(|s| s.to_string()), + ) + } else if let Some(msg) = error_obj.as_str() { + ("UNKNOWN_ERROR".to_string(), msg.to_string(), None) } else { - e + ("UNKNOWN_ERROR".to_string(), "Failed to retrieve agent architecture".to_string(), None) } - }) + } else { + ( + "UNKNOWN_ERROR".to_string(), + response + .get("message") + .and_then(|m| m.as_str()) + .unwrap_or("Failed to retrieve agent architecture") + .to_string(), + None, + ) + }; + + return Err(RunAgentError::execution( + code, + message, + suggestion, + Some(response), + )); + } + + Ok(response) } /// Health check diff --git a/runagent-rust/runagent/src/client/runagent_client.rs b/runagent-rust/runagent/src/client/runagent_client.rs index 857505d..8fbbfd4 100644 --- a/runagent-rust/runagent/src/client/runagent_client.rs +++ b/runagent-rust/runagent/src/client/runagent_client.rs @@ -21,172 +21,261 @@ pub struct RunAgentClient { socket_client: SocketClient, serializer: CoreSerializer, agent_architecture: Option, + extra_params: Option>, #[cfg(feature = "db")] + #[allow(dead_code)] // Reserved for future use db_service: Option, } -impl RunAgentClient { - /// Create a new RunAgent client - /// - /// For local agents, tries to lookup host/port from database if available. - /// For remote agents, configuration is loaded from environment variables. - pub async fn new( - agent_id: &str, - entrypoint_tag: &str, - local: bool, - ) -> RunAgentResult { - #[cfg(feature = "db")] - { - if local { - // Try database lookup first - let db_service = DatabaseService::new(None).await?; - if let Some(agent_info) = db_service.get_agent(agent_id).await? { - tracing::info!("🔍 Found agent in database: {}:{}", agent_info.host, agent_info.port); - return Self::with_address( - agent_id, - entrypoint_tag, - local, - Some(&agent_info.host), - Some(agent_info.port as u16) - ).await; - } else { - return Err(RunAgentError::validation(format!( - "Agent {} not found in local DB. Use with_address() to connect directly.", - agent_id - ))); - } - } +/// Configuration for creating a RunAgent client +/// +/// All fields except `agent_id` and `entrypoint_tag` are optional. +/// +/// # Direct Construction +/// +/// ```rust,no_run +/// use runagent::RunAgentClient; +/// +/// let client = RunAgentClient::new(runagent::RunAgentClientConfig { +/// agent_id: "agent-id".to_string(), +/// entrypoint_tag: "entrypoint".to_string(), +/// local: None, +/// host: None, +/// port: None, +/// api_key: Some("key".to_string()), +/// base_url: Some("http://localhost:8333/".to_string()), +/// extra_params: None, +/// enable_registry: None, +/// }).await?; +/// ``` +/// +/// # Builder Pattern (Alternative) +/// +/// ```rust,no_run +/// let client = RunAgentClient::new( +/// runagent::RunAgentClientConfig::new("agent-id", "entrypoint") +/// .with_api_key("key") +/// .with_base_url("http://localhost:8333/") +/// ).await?; +/// ``` +#[derive(Debug, Clone)] +pub struct RunAgentClientConfig { + /// Agent ID (required) + pub agent_id: String, + /// Entrypoint tag (required) + pub entrypoint_tag: String, + /// Whether this is a local agent (default: false) + pub local: Option, + /// Host for local agents (optional, will lookup from DB if not provided and local=true) + pub host: Option, + /// Port for local agents (optional, will lookup from DB if not provided and local=true) + pub port: Option, + /// API key for remote agents (optional, can also use RUNAGENT_API_KEY env var) + pub api_key: Option, + /// Base URL for remote agents (optional, defaults to https://backend.run-agent.ai) + pub base_url: Option, + /// Extra parameters for future use + pub extra_params: Option>, + /// Enable database registry lookup (default: true for local agents) + pub enable_registry: Option, +} + +impl RunAgentClientConfig { + /// Create a new config with required fields + pub fn new(agent_id: impl Into, entrypoint_tag: impl Into) -> Self { + Self { + agent_id: agent_id.into(), + entrypoint_tag: entrypoint_tag.into(), + local: None, + host: None, + port: None, + api_key: None, + base_url: None, + extra_params: None, + enable_registry: None, } - - #[cfg(not(feature = "db"))] - { - if local { - return Err(RunAgentError::config( - "Database feature not enabled. Use with_address() to connect directly." - )); - } + } + + /// Create a config with defaults for optional fields + /// + /// This allows you to use `..RunAgentClientConfig::default()` syntax + /// to omit None values when constructing directly. + pub fn default() -> Self { + Self { + agent_id: String::new(), // Dummy - will be overridden + entrypoint_tag: String::new(), // Dummy - will be overridden + local: None, + host: None, + port: None, + api_key: None, + base_url: None, + extra_params: None, + enable_registry: None, } - - // For remote connections, proceed without database lookup - Self::with_address(agent_id, entrypoint_tag, local, None, None).await } - /// Create a new RunAgent client with specific host and port - pub async fn with_address( - agent_id: &str, - entrypoint_tag: &str, - local: bool, - host: Option<&str>, - port: Option, - ) -> RunAgentResult { - let serializer = CoreSerializer::new(10.0)?; + /// Set local flag + pub fn with_local(mut self, local: bool) -> Self { + self.local = Some(local); + self + } - #[cfg(feature = "db")] - let db_service = if local { - Some(DatabaseService::new(None).await?) - } else { - None - }; + /// Set host and port for local agents + pub fn with_address(mut self, host: impl Into, port: u16) -> Self { + self.host = Some(host.into()); + self.port = Some(port); + self + } + + /// Set API key + pub fn with_api_key(mut self, api_key: impl Into) -> Self { + self.api_key = Some(api_key.into()); + self + } + + /// Set base URL + pub fn with_base_url(mut self, base_url: impl Into) -> Self { + self.base_url = Some(base_url.into()); + self + } + + /// Set extra parameters + pub fn with_extra_params(mut self, extra_params: HashMap) -> Self { + self.extra_params = Some(extra_params); + self + } + + /// Enable or disable registry lookup + pub fn with_enable_registry(mut self, enable: bool) -> Self { + self.enable_registry = Some(enable); + self + } +} + +impl RunAgentClient { + /// Create a new RunAgent client from configuration + /// + /// This is the single entry point for creating clients. + /// + /// # Examples + /// + /// ```rust,no_run + /// // Local agent with explicit address + /// let client = RunAgentClient::new(RunAgentClientConfig::new("agent-id", "entrypoint") + /// .with_local(true) + /// .with_address("127.0.0.1", 8450) + /// .with_enable_registry(false) + /// ).await?; + /// + /// // Remote agent + /// let client = RunAgentClient::new(RunAgentClientConfig::new("agent-id", "entrypoint") + /// .with_api_key(env::var("RUNAGENT_API_KEY").unwrap()) + /// ).await?; + /// ``` + pub async fn new(config: RunAgentClientConfig) -> RunAgentResult { + use crate::constants::{DEFAULT_BASE_URL, ENV_RUNAGENT_API_KEY, ENV_RUNAGENT_BASE_URL}; - let (rest_client, socket_client) = if local { - let (agent_host, agent_port) = if let (Some(h), Some(p)) = (host, port) { - tracing::info!("🔌 Using explicit address: {}:{}", h, p); - (h.to_string(), p) - } else { + let local = config.local.unwrap_or(false); + let enable_registry = config.enable_registry.unwrap_or(local); + + // Resolve host/port for local agents + let (host, port) = if local { + // If host/port provided, use them + if let (Some(h), Some(p)) = (&config.host, &config.port) { + (Some(h.clone()), Some(*p)) + } else if enable_registry { + // Try database lookup if enabled #[cfg(feature = "db")] { - if let Some(ref db) = db_service { - let agent_info = db.get_agent(agent_id).await? - .ok_or_else(|| RunAgentError::validation(format!("Agent {} not found in local DB", agent_id)))?; - - tracing::info!("🔍 Auto-resolved address for agent {}: {}:{}", agent_id, agent_info.host, agent_info.port); - (agent_info.host, agent_info.port as u16) + let db_service = DatabaseService::new(None).await?; + if let Some(agent_info) = db_service.get_agent(&config.agent_id).await? { + tracing::info!("🔍 Found agent in database: {}:{}", agent_info.host, agent_info.port); + (Some(agent_info.host), Some(agent_info.port as u16)) } else { - return Err(RunAgentError::config("Database feature not enabled but required for local agent lookup")); + (config.host.clone(), config.port) } } #[cfg(not(feature = "db"))] { - return Err(RunAgentError::config("Database feature not enabled but required for local agent lookup")); + (config.host.clone(), config.port) } - }; + } else { + (config.host.clone(), config.port) + } + } else { + (None, None) + }; - let agent_base_url = format!("http://{}:{}", agent_host, agent_port); - let agent_socket_url = format!("ws://{}:{}", agent_host, agent_port); + // Resolve API key (config > env var) + let api_key = config.api_key.or_else(|| { + std::env::var(ENV_RUNAGENT_API_KEY).ok() + }); + + // Resolve base URL (config > env var > default) + let base_url = config.base_url.or_else(|| { + std::env::var(ENV_RUNAGENT_BASE_URL).ok() + }).unwrap_or_else(|| DEFAULT_BASE_URL.to_string()); + + let serializer = CoreSerializer::new(10.0)?; + #[cfg(feature = "db")] + let db_service: Option = None; + #[cfg(not(feature = "db"))] + let db_service: Option = None; + + let (rest_client, socket_client) = if local { + let host = host.ok_or_else(|| { + RunAgentError::validation( + "Host is required for local clients. Provide host/port in config or enable registry for database lookup.", + ) + })?; + let port = port.ok_or_else(|| { + RunAgentError::validation( + "Port is required for local clients. Provide host/port in config or enable registry for database lookup.", + ) + })?; + + tracing::info!("🔌 Using address: {}:{}", host, port); + + let agent_base_url = format!("http://{}:{}", host, port); + let agent_socket_url = format!("ws://{}:{}", host, port); let rest_client = RestClient::new(&agent_base_url, None, Some("/api/v1"))?; let socket_client = SocketClient::new(&agent_socket_url, None, Some("/api/v1"))?; (rest_client, socket_client) } else { - let rest_client = RestClient::default()?; - let socket_client = SocketClient::default()?; - (rest_client, socket_client) + Self::create_remote_clients(Some(&base_url), api_key)? }; let mut client = Self { - agent_id: agent_id.to_string(), - entrypoint_tag: entrypoint_tag.to_string(), + agent_id: config.agent_id, + entrypoint_tag: config.entrypoint_tag, local, rest_client, socket_client, serializer, agent_architecture: None, + extra_params: config.extra_params, #[cfg(feature = "db")] db_service, }; - // Get agent architecture (skip validation to match Python SDK behavior) - match client.get_agent_architecture_internal().await { - Ok(architecture) => { - client.agent_architecture = Some(architecture); - tracing::debug!("Agent architecture loaded, skipping client-side validation"); - } - Err(e) => { - tracing::debug!("Failed to get agent architecture, skipping validation: {}", e); - // Set a minimal architecture to avoid validation errors - client.agent_architecture = Some(serde_json::json!({ - "entrypoints": [{"tag": "simulate_stream", "file": "main.py", "module": "simulate_stream"}] - })); - } - } + client.initialize_architecture().await?; Ok(client) } + async fn initialize_architecture(&mut self) -> RunAgentResult<()> { + let architecture = self.get_agent_architecture_internal().await?; + self.agent_architecture = Some(architecture); + self.validate_entrypoint()?; + Ok(()) + } + async fn get_agent_architecture_internal(&self) -> RunAgentResult { - match self.rest_client.get_agent_architecture(&self.agent_id).await { - Ok(architecture) => Ok(architecture), - Err(_) => { - // Fallback: provide default architecture with common entrypoints - Ok(serde_json::json!({ - "entrypoints": [ - { - "tag": "generic", - "file": "main.py", - "module": "run" - }, - { - "tag": "generic_stream", - "file": "main.py", - "module": "run_stream" - }, - { - "tag": "simulate_stream", - "file": "main.py", - "module": "simulate_stream" - }, - { - "tag": "run", - "file": "main.py", - "module": "run" - } - ] - })) - } - } + self.rest_client.get_agent_architecture(&self.agent_id).await } fn validate_entrypoint(&self) -> RunAgentResult<()> { @@ -200,6 +289,17 @@ impl RunAgentClient { }); if !found { + let available: Vec = entrypoints + .iter() + .filter_map(|ep| ep.get("tag").and_then(|t| t.as_str())) + .map(|s| s.to_string()) + .collect(); + tracing::error!( + "Entrypoint `{}` not found for agent {}. Available: {:?}", + self.entrypoint_tag, + self.agent_id, + available + ); return Err(RunAgentError::validation(format!( "Entrypoint `{}` not found in agent {}", self.entrypoint_tag, self.agent_id @@ -243,51 +343,44 @@ impl RunAgentClient { .await?; if response.get("success").and_then(|s| s.as_bool()).unwrap_or(false) { + // Process response data + let mut payload: Option = None; + if let Some(data) = response.get("data") { - // Simplified payload: data is a structured output string - if let Some(data_str) = data.as_str() { - if let Ok(parsed) = serde_json::from_str::(data_str) { - return self.serializer.deserialize_object(parsed); - } - return self - .serializer - .deserialize_object(Value::String(data_str.to_string())); + // Case 1: data is a string (simplified payload - could be JSON string with {type, payload}) + if data.as_str().is_some() { + // Use common deserializer preparation logic + let prepared = self.serializer.prepare_for_deserialization(data.clone()); + payload = Some(prepared); } - - // Legacy detailed execution payload - if let Some(result_data) = data.get("result_data") { + // Case 2: data has result_data.data (legacy detailed execution payload) + else if let Some(result_data) = data.get("result_data") { if let Some(output_data) = result_data.get("data") { - // Check if the output contains a generator object string - if let Some(content_str) = output_data.as_str() { - if content_str.contains("generator object") { - tracing::warn!("Agent returned generator object instead of content. Consider using streaming endpoint for this agent."); - // Return the raw string for now - return Ok(output_data.clone()); - } - // If it's a JSON string, try to parse it first - if let Ok(parsed) = serde_json::from_str::(content_str) { - return self.serializer.deserialize_object(parsed); - } - } - return self.serializer.deserialize_object(output_data.clone()); + payload = Some(output_data.clone()); } } + // Case 3: data is an object (could be {type, payload} structure) + else if data.is_object() { + payload = Some(data.clone()); + } } - // Fallback to old format for backward compatibility - if let Some(output_data) = response.get("output_data") { - // Check if the output contains a generator object string - if let Some(content_str) = output_data.as_str() { + // Case 4: Fallback to output_data (backward compatibility) + else if let Some(output_data) = response.get("output_data") { + payload = Some(output_data.clone()); + } + + // Deserialize the payload using serializer (handles {type, payload} structure) + if let Some(payload_val) = payload { + // Check for generator object warning + if let Some(content_str) = payload_val.as_str() { if content_str.contains("generator object") { tracing::warn!("Agent returned generator object instead of content. Consider using streaming endpoint for this agent."); - // Return the raw string for now - return Ok(output_data.clone()); - } - // If it's a JSON string, try to parse it first - if let Ok(parsed) = serde_json::from_str::(content_str) { - return self.serializer.deserialize_object(parsed); + return Ok(payload_val); } } - return self.serializer.deserialize_object(output_data.clone()); + // Deserialize the payload - this should extract payload from {type, payload} structure + let deserialized = self.serializer.deserialize_object(payload_val)?; + return Ok(deserialized); } Ok(Value::Null) } else { @@ -324,6 +417,12 @@ impl RunAgentClient { input_args: &[Value], input_kwargs: &[(&str, Value)], ) -> RunAgentResult> + Send>>> { + if !self.entrypoint_tag.ends_with("_stream") { + return Err(RunAgentError::validation( + "Use run() for non-stream entrypoints".to_string(), + )); + } + let input_kwargs_map: HashMap = input_kwargs .iter() .map(|(k, v)| (k.to_string(), v.clone())) @@ -357,8 +456,37 @@ impl RunAgentClient { &self.entrypoint_tag } + /// Get any extra params supplied during initialization + pub fn extra_params(&self) -> Option<&HashMap> { + self.extra_params.as_ref() + } + /// Check if using local deployment pub fn is_local(&self) -> bool { self.local } +} + +impl RunAgentClient { + fn create_remote_clients( + base_url_override: Option<&str>, + api_key_override: Option, + ) -> RunAgentResult<(RestClient, SocketClient)> { + if let Some(base_url) = base_url_override { + let rest_client = RestClient::new(base_url, api_key_override.clone(), Some("/api/v1"))?; + let socket_base = if base_url.starts_with("https://") { + base_url.replace("https://", "wss://") + } else if base_url.starts_with("http://") { + base_url.replace("http://", "ws://") + } else { + format!("wss://{}", base_url) + }; + let socket_client = SocketClient::new(&socket_base, api_key_override, Some("/api/v1"))?; + Ok((rest_client, socket_client)) + } else { + let rest_client = RestClient::default()?; + let socket_client = SocketClient::default()?; + Ok((rest_client, socket_client)) + } + } } \ No newline at end of file diff --git a/runagent-rust/runagent/src/client/socket_client.rs b/runagent-rust/runagent/src/client/socket_client.rs index 2ca94f0..6520134 100644 --- a/runagent-rust/runagent/src/client/socket_client.rs +++ b/runagent-rust/runagent/src/client/socket_client.rs @@ -100,6 +100,9 @@ impl SocketClient { write.send(Message::Text(serialized_msg)).await .map_err(|e| RunAgentError::connection(format!("Failed to send start message: {}", e)))?; + // Clone serializer for use in async stream + let serializer = self.serializer.clone(); + // Create stream that processes incoming messages (matching Python SDK behavior) let stream = async_stream::stream! { while let Some(message) = read.next().await { @@ -129,9 +132,22 @@ impl SocketClient { break; } Some("data") => { - // Yield the content field (matching Python SDK) + // Extract content and deserialize it using the common deserializer if let Some(content) = msg.get("content") { - yield Ok(content.clone()); + // Use common deserializer preparation logic (handles JSON strings) + let prepared = serializer.prepare_for_deserialization(content.clone()); + + // Deserialize using the common serializer (handles {type, payload} structure) + match serializer.deserialize_object(prepared) { + Ok(deserialized) => yield Ok(deserialized), + Err(e) => { + yield Err(RunAgentError::server(format!("Deserialization error: {}", e))); + break; + } + } + } else { + // If no content, yield the whole message + yield Ok(msg); } } _ => { diff --git a/runagent-rust/runagent/src/constants.rs b/runagent-rust/runagent/src/constants.rs index 4340c04..3c39eaa 100644 --- a/runagent-rust/runagent/src/constants.rs +++ b/runagent-rust/runagent/src/constants.rs @@ -1,22 +1,6 @@ -//! Constants and configuration values for the RunAgent SDK - -use once_cell::sync::Lazy; -use std::path::PathBuf; - -/// Template repository URL -pub const TEMPLATE_REPO_URL: &str = "https://github.com/runagent-dev/runagent.git"; - -/// Template repository branch -pub const TEMPLATE_BRANCH: &str = "main"; - -/// Template pre-path -pub const TEMPLATE_PREPATH: &str = "templates"; - -/// Default framework -pub const DEFAULT_FRAMEWORK: &str = "langchain"; - -/// Default template -pub const DEFAULT_TEMPLATE: &str = "basic"; +//! Constants for the RunAgent Client SDK +//! +//! Only client-related constants. No CLI features. /// Environment variable for API key pub const ENV_RUNAGENT_API_KEY: &str = "RUNAGENT_API_KEY"; @@ -24,146 +8,15 @@ pub const ENV_RUNAGENT_API_KEY: &str = "RUNAGENT_API_KEY"; /// Environment variable for base URL pub const ENV_RUNAGENT_BASE_URL: &str = "RUNAGENT_BASE_URL"; -/// Environment variable for cache directory -pub const ENV_LOCAL_CACHE_DIRECTORY: &str = "RUNAGENT_CACHE_DIR"; +/// Default base URL for remote agents +pub const DEFAULT_BASE_URL: &str = "https://backend.run-agent.ai"; -/// Environment variable for logging level -pub const ENV_RUNAGENT_LOGGING_LEVEL: &str = "RUNAGENT_LOGGING_LEVEL"; +/// Default API prefix +pub const DEFAULT_API_PREFIX: &str = "/api/v1"; -/// Default base URL -pub const DEFAULT_BASE_URL: &str = "https://backend.run-agent.ai/"; +/// Default timeout for agent execution (5 minutes) +pub const DEFAULT_TIMEOUT_SECONDS: u64 = 300; -/// Agent config file name +/// Agent config file name (for reading agent configs, not for creating them) pub const AGENT_CONFIG_FILE_NAME: &str = "runagent.config.json"; -/// User data file name -pub const USER_DATA_FILE_NAME: &str = "user_data.json"; - -/// Default local cache directory path -const LOCAL_CACHE_DIRECTORY_PATH: &str = "~/.runagent"; - -/// Default port range for local servers -pub const DEFAULT_PORT_START: u16 = 8450; -pub const DEFAULT_PORT_END: u16 = 8500; - -/// Database file name -pub const DATABASE_FILE_NAME: &str = "runagent_local.db"; - -/// Maximum number of local agents -pub const MAX_LOCAL_AGENTS: usize = 5; - -/// Local cache directory (computed at runtime) - FIXED to match Python exactly -pub static LOCAL_CACHE_DIRECTORY: Lazy = Lazy::new(|| { - // Check environment variable first - if let Ok(env_path) = std::env::var(ENV_LOCAL_CACHE_DIRECTORY) { - return PathBuf::from(env_path); - } - - // Use the same logic as Python: always ~/.runagent - // Python: os.path.expanduser("~/.runagent") - dirs::home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(".runagent") -}); - -/// Supported frameworks -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Framework { - LangGraph, - LangChain, - LlamaIndex, - CrewAI, - AutoGen, - Default, -} - -impl Framework { - pub fn as_str(&self) -> &'static str { - match self { - Framework::LangGraph => "langgraph", - Framework::LangChain => "langchain", - Framework::LlamaIndex => "llamaindex", - Framework::CrewAI => "crewai", - Framework::AutoGen => "autogen", - Framework::Default => "default", - } - } - - pub fn from_str(s: &str) -> Option { - match s.to_lowercase().as_str() { - "langgraph" => Some(Framework::LangGraph), - "langchain" => Some(Framework::LangChain), - "llamaindex" => Some(Framework::LlamaIndex), - "crewai" => Some(Framework::CrewAI), - "autogen" => Some(Framework::AutoGen), - "default" => Some(Framework::Default), - _ => None, - } - } -} - -/// Template variants -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TemplateVariant { - Basic, - Advanced, -} - -impl TemplateVariant { - pub fn as_str(&self) -> &'static str { - match self { - TemplateVariant::Basic => "basic", - TemplateVariant::Advanced => "advanced", - } - } - - pub fn from_str(s: &str) -> Option { - match s.to_lowercase().as_str() { - "basic" => Some(TemplateVariant::Basic), - "advanced" => Some(TemplateVariant::Advanced), - _ => None, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_framework_conversion() { - assert_eq!(Framework::LangChain.as_str(), "langchain"); - assert_eq!(Framework::from_str("langchain"), Some(Framework::LangChain)); - assert_eq!(Framework::from_str("invalid"), None); - } - - #[test] - fn test_template_variant_conversion() { - assert_eq!(TemplateVariant::Basic.as_str(), "basic"); - assert_eq!(TemplateVariant::from_str("basic"), Some(TemplateVariant::Basic)); - assert_eq!(TemplateVariant::from_str("invalid"), None); - } - - #[test] - fn test_cache_directory_matches_python() { - // Test that our path matches what Python would generate - let expected = dirs::home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(".runagent"); - - let actual = &*LOCAL_CACHE_DIRECTORY; - assert_eq!(*actual, expected); - } - - #[test] - fn test_database_path() { - let db_path = LOCAL_CACHE_DIRECTORY.join(DATABASE_FILE_NAME); - let expected = dirs::home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(".runagent") - .join("runagent_local.db"); - - assert_eq!(db_path, expected); - } -} - diff --git a/runagent-rust/runagent/src/db/service.rs b/runagent-rust/runagent/src/db/service.rs index bde6d96..705c8a6 100644 --- a/runagent-rust/runagent/src/db/service.rs +++ b/runagent-rust/runagent/src/db/service.rs @@ -1,10 +1,29 @@ //! Database service for agent lookups -use crate::constants::{DATABASE_FILE_NAME, LOCAL_CACHE_DIRECTORY}; use crate::types::{RunAgentError, RunAgentResult}; +use once_cell::sync::Lazy; use sqlx::{sqlite::SqlitePool, Row}; use std::path::PathBuf; +/// Database file name +const DATABASE_FILE_NAME: &str = "runagent_local.db"; + +/// Environment variable for cache directory +const ENV_LOCAL_CACHE_DIRECTORY: &str = "RUNAGENT_CACHE_DIR"; + +/// Local cache directory (computed at runtime) +static LOCAL_CACHE_DIRECTORY: Lazy = Lazy::new(|| { + // Check environment variable first + if let Ok(env_path) = std::env::var(ENV_LOCAL_CACHE_DIRECTORY) { + return PathBuf::from(env_path); + } + + // Default to ~/.runagent + dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".runagent") +}); + /// Agent information stored in database #[derive(Debug, Clone)] pub struct AgentInfo { diff --git a/runagent-rust/runagent/src/lib.rs b/runagent-rust/runagent/src/lib.rs index a525854..324f28f 100644 --- a/runagent-rust/runagent/src/lib.rs +++ b/runagent-rust/runagent/src/lib.rs @@ -234,10 +234,36 @@ pub mod utils; #[cfg(feature = "db")] pub mod db; +/// Blocking (synchronous) wrapper for RunAgentClient +/// +/// This module provides a synchronous interface that wraps the async client. +/// It's useful for simple scripts or when you can't use async/await. +/// +/// # Example +/// +/// ```rust,no_run +/// use runagent::blocking::{RunAgentClient, RunAgentClientConfig}; +/// use serde_json::json; +/// +/// fn main() -> runagent::RunAgentResult<()> { +/// let client = RunAgentClient::new( +/// RunAgentClientConfig::new("agent-id", "entrypoint") +/// .with_api_key(Some("key".to_string())) +/// )?; +/// +/// let result = client.run(&[("message", json!("Hello"))])?; +/// Ok(()) +/// } +/// ``` +pub mod blocking; + // Re-export commonly used types and functions -pub use client::{RunAgentClient, RestClient, SocketClient}; +pub use client::{RunAgentClient, RunAgentClientConfig, RestClient, SocketClient}; pub use types::{RunAgentError, RunAgentResult}; +// Re-export blocking client for convenience +pub use blocking::RunAgentClient as BlockingRunAgentClient; + #[cfg(feature = "db")] pub use db::DatabaseService; @@ -387,9 +413,8 @@ impl RunAgentConfig { /// // Now you have access to RunAgentClient, RunAgentError, etc. /// ``` pub mod prelude { - pub use crate::client::{RunAgentClient, RestClient, SocketClient}; + pub use crate::client::{RunAgentClient, RunAgentClientConfig, RestClient, SocketClient}; pub use crate::types::{RunAgentError, RunAgentResult}; - pub use crate::RunAgentConfig; #[cfg(feature = "db")] pub use crate::db::DatabaseService; diff --git a/runagent-rust/runagent/src/types/errors.rs b/runagent-rust/runagent/src/types/errors.rs index 950efe5..ed75320 100644 --- a/runagent-rust/runagent/src/types/errors.rs +++ b/runagent-rust/runagent/src/types/errors.rs @@ -1,5 +1,6 @@ //! Error types for the RunAgent SDK +use serde_json::Value; use std::fmt; use thiserror::Error; @@ -38,6 +39,15 @@ pub enum RunAgentError { #[error("Configuration error: {message}")] Config { message: String }, + /// Execution/SDK errors with structured details + #[error("{code}: {message}")] + Execution { + code: String, + message: String, + suggestion: Option, + details: Option, + }, + /// IO errors #[error("IO error: {0}")] Io(#[from] std::io::Error), @@ -112,6 +122,21 @@ impl RunAgentError { } } + /// Create a new execution error with structured metadata + pub fn execution>( + code: S, + message: S, + suggestion: Option, + details: Option, + ) -> Self { + Self::Execution { + code: code.into(), + message: message.into(), + suggestion, + details, + } + } + /// Create a new generic error pub fn generic>(message: S) -> Self { Self::Generic { @@ -130,6 +155,7 @@ impl RunAgentError { Self::Deployment { .. } => "deployment", Self::Database { .. } => "database", Self::Config { .. } => "config", + Self::Execution { .. } => "execution", Self::Io(_) => "io", Self::Json(_) => "json", Self::Http(_) => "http", @@ -142,7 +168,7 @@ impl RunAgentError { matches!( self, Self::Connection { .. } | Self::Server { .. } | Self::Http(_) - ) + ) || matches!(self, Self::Execution { code, .. } if code == "CONNECTION_ERROR" || code == "SERVER_ERROR") } } diff --git a/runagent-rust/runagent/src/types/schema.rs b/runagent-rust/runagent/src/types/schema.rs index 81d1511..55b8f52 100644 --- a/runagent-rust/runagent/src/types/schema.rs +++ b/runagent-rust/runagent/src/types/schema.rs @@ -5,6 +5,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::path::PathBuf; // use uuid::Uuid; /// Template source configuration @@ -279,9 +280,13 @@ pub struct DatabaseConfig { impl Default for DatabaseConfig { fn default() -> Self { + // Use default path for database + let db_path = dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".runagent") + .join("runagent_local.db"); Self { - url: format!("sqlite://{}/runagent_local.db", - crate::constants::LOCAL_CACHE_DIRECTORY.to_string_lossy()), + url: format!("sqlite://{}", db_path.to_string_lossy()), max_connections: 10, min_connections: 1, connect_timeout: 30, diff --git a/runagent-rust/runagent/src/utils/config.rs b/runagent-rust/runagent/src/utils/config.rs index cd0b3ed..07703d0 100644 --- a/runagent-rust/runagent/src/utils/config.rs +++ b/runagent-rust/runagent/src/utils/config.rs @@ -1,8 +1,7 @@ //! Configuration management for the RunAgent SDK use crate::constants::{ - DEFAULT_BASE_URL, ENV_RUNAGENT_API_KEY, ENV_RUNAGENT_BASE_URL, LOCAL_CACHE_DIRECTORY, - USER_DATA_FILE_NAME, + DEFAULT_BASE_URL, ENV_RUNAGENT_API_KEY, ENV_RUNAGENT_BASE_URL, }; use crate::types::{RunAgentError, RunAgentResult}; use serde::{Deserialize, Serialize}; @@ -38,22 +37,12 @@ impl Default for Config { } impl Config { - /// Load configuration from various sources + /// Load configuration from environment variables + /// All configuration is now stored in SQLite database, this only loads from env vars pub fn load() -> RunAgentResult { let mut config = Self::default(); - // 1. Load from config file - if let Ok(file_config) = Self::load_from_file() { - config.api_key = file_config.api_key.or(config.api_key); - config.base_url = if file_config.base_url.is_empty() { - config.base_url - } else { - file_config.base_url - }; - config.user_info.extend(file_config.user_info); - } - - // 2. Override with environment variables + // Load from environment variables if let Ok(env_api_key) = std::env::var(ENV_RUNAGENT_API_KEY) { config.api_key = Some(env_api_key); } @@ -62,7 +51,7 @@ impl Config { config.base_url = env_base_url; } - // 3. Ensure base_url has proper format + // Ensure base_url has proper format if !config.base_url.starts_with("http://") && !config.base_url.starts_with("https://") { config.base_url = format!("https://{}", config.base_url); } @@ -70,47 +59,12 @@ impl Config { Ok(config) } - /// Load configuration from file - fn load_from_file() -> RunAgentResult { - let config_path = Self::get_config_file_path(); - - if config_path.exists() { - let content = fs::read_to_string(&config_path) - .map_err(|e| RunAgentError::config(format!("Failed to read config file: {}", e)))?; - - match serde_json::from_str::(&content) { - Ok(parsed_config) => Ok(parsed_config), - Err(_) => Ok(Self::default()) - } - } else { - Ok(Self::default()) - } - } - - /// Save configuration to file - pub fn save(&self) -> RunAgentResult<()> { - let config_path = Self::get_config_file_path(); - - // Create parent directories if they don't exist - if let Some(parent) = config_path.parent() { - fs::create_dir_all(parent) - .map_err(|e| RunAgentError::config(format!("Failed to create config directory: {}", e)))?; - } - - let content = serde_json::to_string_pretty(self) - .map_err(|e| RunAgentError::config(format!("Failed to serialize config: {}", e)))?; - - fs::write(&config_path, content) - .map_err(|e| RunAgentError::config(format!("Failed to write config file: {}", e)))?; - - Ok(()) - } - /// Setup and validate configuration + /// Note: Configuration is stored in SQLite database, not in files pub fn setup( api_key: Option, base_url: Option, - save: bool, + _save: bool, ) -> RunAgentResult { let mut config = Self::load()?; @@ -137,10 +91,7 @@ impl Config { return Err(RunAgentError::authentication("Authentication failed with provided credentials")); } - // Save if requested - if save { - config.save()?; - } + // Note: save parameter is ignored - configuration is stored in SQLite database Ok(config) } @@ -171,24 +122,10 @@ impl Config { status.insert("api_key_set".to_string(), serde_json::json!(self.api_key.is_some())); status.insert("base_url".to_string(), serde_json::json!(self.base_url)); status.insert("user_info".to_string(), serde_json::json!(self.user_info)); - status.insert("config_file".to_string(), serde_json::json!(Self::get_config_file_path())); - status.insert("config_file_exists".to_string(), serde_json::json!(Self::get_config_file_path().exists())); status } - /// Clear all configuration - pub fn clear() -> RunAgentResult<()> { - let config_path = Self::get_config_file_path(); - - if config_path.exists() { - fs::remove_file(&config_path) - .map_err(|e| RunAgentError::config(format!("Failed to remove config file: {}", e)))?; - } - - Ok(()) - } - /// Get API key pub fn api_key(&self) -> Option { self.api_key.clone() @@ -204,11 +141,6 @@ impl Config { &self.user_info } - /// Get config file path - fn get_config_file_path() -> PathBuf { - LOCAL_CACHE_DIRECTORY.join(USER_DATA_FILE_NAME) - } - /// Create agent configuration file pub fn create_agent_config( project_dir: &str, @@ -270,66 +202,6 @@ impl Config { Ok(Some(config)) } - /// Backup current configuration - pub fn backup() -> RunAgentResult> { - let config_path = Self::get_config_file_path(); - - if !config_path.exists() { - return Ok(None); - } - - let config = Self::load_from_file()?; - - // Remove sensitive data from backup - let mut backup_config = config; - backup_config.api_key = None; - - let backup_dir = LOCAL_CACHE_DIRECTORY.join("backups"); - fs::create_dir_all(&backup_dir) - .map_err(|e| RunAgentError::config(format!("Failed to create backup directory: {}", e)))?; - - let timestamp = chrono::Utc::now().timestamp(); - let backup_file = backup_dir.join(format!("config_backup_{}.json", timestamp)); - - let content = serde_json::to_string_pretty(&backup_config) - .map_err(|e| RunAgentError::config(format!("Failed to serialize backup: {}", e)))?; - - fs::write(&backup_file, content) - .map_err(|e| RunAgentError::config(format!("Failed to write backup: {}", e)))?; - - Ok(Some(backup_file.to_string_lossy().to_string())) - } - - /// Set user configuration value - pub fn set_user_config(key: &str, value: serde_json::Value) -> RunAgentResult<()> { - let mut config = Self::load()?; - config.user_info.insert(key.to_string(), value); - config.save() - } - - /// Get user configuration value - pub fn get_user_config(key: &str) -> RunAgentResult> { - let config = Self::load()?; - Ok(config.user_info.get(key).cloned()) - } - - /// Set base URL - pub fn set_base_url(base_url: &str) -> RunAgentResult<()> { - let mut config = Self::load()?; - config.base_url = if base_url.starts_with("http://") || base_url.starts_with("https://") { - base_url.to_string() - } else { - format!("https://{}", base_url) - }; - config.save() - } - - /// Set API key - pub fn set_api_key(api_key: &str) -> RunAgentResult<()> { - let mut config = Self::load()?; - config.api_key = Some(api_key.to_string()); - config.save() - } /// Save deployment information pub fn save_deployment_info( diff --git a/runagent-rust/runagent/src/utils/serializer.rs b/runagent-rust/runagent/src/utils/serializer.rs index 2341c89..5471306 100644 --- a/runagent-rust/runagent/src/utils/serializer.rs +++ b/runagent-rust/runagent/src/utils/serializer.rs @@ -30,8 +30,72 @@ impl CoreSerializer { Ok(json_str) } - /// Deserialize JSON string to object + /// Prepare value for deserialization + /// + /// If the value is a JSON string, parses it first. + /// Otherwise returns the value as-is. + /// This handles cases where responses come as JSON strings. + pub fn prepare_for_deserialization(&self, value: Value) -> Value { + if let Some(str_val) = value.as_str() { + // Try to parse as JSON first + match serde_json::from_str::(str_val) { + Ok(parsed) => parsed, + Err(_) => value, // Not JSON, return as-is + } + } else { + value // Already parsed + } + } + + /// Deserialize JSON response to object + /// + /// Handles multiple response formats: + /// 1. `{type, payload}` structure - extracts and deserializes payload + /// 2. String payload - parses JSON string + /// 3. Direct value - reconstructs nested JSON + /// + /// Handles multiple response formats including `{type, payload}` structures. pub fn deserialize_object(&self, json_resp: Value) -> RunAgentResult { + // Handle {type, payload} structure + if let Value::Object(ref map) = json_resp { + if map.contains_key("type") && map.contains_key("payload") { + let payload_val = map.get("payload").unwrap(); + + // If payload is a string, try to parse it as JSON + if let Some(payload_str) = payload_val.as_str() { + // The payload is a JSON-encoded string, parse it to get the actual value + // Example: payload_str = "\"Hello\"" -> parsed = "Hello" + match serde_json::from_str::(payload_str) { + Ok(parsed) => { + // Parse succeeded - return the parsed value + return Ok(parsed); + } + Err(_) => { + // Parse failed - return the string as-is + return Ok(Value::String(payload_str.to_string())); + } + } + } + + // Payload is not a string - reconstruct it directly + return self.reconstruct_nested_json(payload_val.clone()); + } + + // Handle {content} structure (legacy format) + if let Some(content) = map.get("content") { + return self.reconstruct_nested_json(content.clone()); + } + } + + // If it's a string, try to parse it first + if let Some(str_val) = json_resp.as_str() { + match serde_json::from_str::(str_val) { + Ok(parsed) => return self.reconstruct_nested_json(parsed), + Err(_) => return Ok(Value::String(str_val.to_string())), + } + } + + // Default: reconstruct nested JSON self.reconstruct_nested_json(json_resp) } diff --git a/sdk_checklist.md b/sdk_checklist.md index ac6c9d7..3fba57e 100644 --- a/sdk_checklist.md +++ b/sdk_checklist.md @@ -42,6 +42,13 @@ - Allow overrides via constructor or `RUNAGENT_BASE_URL` env var. - Respect per-request overrides for self-hosted or staging deployments. +### Architecture Metadata Contract +- Treat `/api/v1/agents/{id}/architecture` as an envelope `{ success, data, message, error, timestamp, request_id }`. Handle both the new envelope and the legacy payload transparently. +- When `success === false`, raise the backend-provided `code/message` and surface any `suggestion`/`details` so users know how to recover (e.g. `AGENT_NOT_FOUND_REMOTE`, `AUTHENTICATION_ERROR`). +- When `success === true`, normalize `data` into a single `AgentArchitecture` structure that includes `agent_id`/`agentId` plus the full entrypoint metadata (`tag`, `file`, `module`, `extractor`, `description`, etc.). +- If `data` or `data.entrypoints` is missing, throw a clear `ARCHITECTURE_MISSING` error instructing users to redeploy or supply proper entrypoints. +- When an entrypoint lookup fails, log or expose the list of entrypoint tags returned by the server to simplify debugging typo/mismatch issues. + ### Authentication - Use Bearer tokens everywhere; reuse the CLI convention (`Authorization: Bearer ${api_key}`) and query-string token fallback for WebSockets. - Environment variable name: `RUNAGENT_API_KEY`. @@ -135,6 +142,13 @@ 6. Advanced topics (custom base URL, extra params, retries). 7. Troubleshooting (common connection/auth issues). +### Additional Consistency Requirements +- **Architecture endpoint contract**: every SDK must treat `/api/v1/agents/{id}/architecture` as an envelope `{ success, data, message, error, timestamp, request_id }`, propagate backend `error.code/message/suggestion/details`, normalize the `data` payload (including `agent_id`/`agentId`, `file`, `module`, `extractor`, etc.), and throw a clear `ARCHITECTURE_MISSING` error when `data.entrypoints` is absent. +- **Run vs. runStream guardrails**: enforce that `_stream` tags only work with `runStream()` (`STREAM_ENTRYPOINT` error with a helpful suggestion) and non-stream tags only work with `run()` (`NON_STREAM_ENTRYPOINT` error). This mirrors the CLI and prevents silent misuse. +- **Structured error surfaces**: expose a canonical error type (`RunAgentError`/`RunAgentExecutionError`) that always carries `code`, `message`, `suggestion`, and optional `details`, and reuse the shared code taxonomy (`AUTHENTICATION_ERROR`, `PERMISSION_ERROR`, `VALIDATION_ERROR`, `CONNECTION_ERROR`, `SERVER_ERROR`, `AGENT_NOT_FOUND_LOCAL`, `AGENT_NOT_FOUND_REMOTE`, `STREAM_ENTRYPOINT`, `NON_STREAM_ENTRYPOINT`, `ARCHITECTURE_MISSING`, etc.). +- **Diagnostics for entrypoint mismatches**: when the requested entrypoint isn’t found, log or otherwise expose the set of tags returned by the backend so developers can quickly spot typos or missing deployments. +- **Repository hygiene**: every SDK repo must ship a `README` (installation, configuration, local vs. remote, run vs. runStream examples) and a `PUBLISH.md` (version bump instructions, build/test checklist, npm publish guidance). This keeps packaging and documentation consistent across languages. + ### References ```24:37:runagent/constants.py LOCAL_CACHE_DIRECTORY_PATH = "~/.runagent"