Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
Cargo.lock
26 changes: 26 additions & 0 deletions agent/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[workspace]

[package]
name = "iii-agent"
version = "0.1.0"
edition = "2021"
publish = false

[[bin]]
name = "iii-agent"
path = "src/main.rs"

[dependencies]
iii-sdk = "=0.11.3"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
clap = { version = "4", features = ["derive", "env"] }
chrono = { version = "0.4", features = ["serde"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] }
futures-util = "0.3"
uuid = { version = "1", features = ["v4"] }
70 changes: 70 additions & 0 deletions agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# iii-agent

Linear, PostHog, Attio — they all shipped the same thing: a chat bar as the primary interface. iii-agent brings this to the iii console. It dynamically discovers every function registered by every connected worker, lets users ask questions in natural language, and the LLM decides which functions to call. "What's slow in my system?" triggers `eval::analyze_traces`. "Show me the topology" triggers `introspect::diagram`. The agent composes the answer from real data, not hallucinations.

**Plug and play:** Build with `cargo build --release`, set `ANTHROPIC_API_KEY` in your environment, then run `./target/release/iii-agent --url ws://your-engine:49134`. It registers 7 functions, discovers all available tools from other workers, and starts accepting chat via `agent::chat`. Connect more workers and they're automatically available — no restart needed.

## Functions

| Function ID | Description |
|---|---|
| `agent::chat` | Send a message and get a structured JSON-UI response |
| `agent::chat_stream` | Send a message with streaming response via iii Streams |
| `agent::discover` | List all available functions the agent can orchestrate |
| `agent::plan` | Generate an execution plan DAG without executing |
| `agent::session_create` | Create a new chat session |
| `agent::session_history` | Retrieve conversation history for a session |
| `agent::session_cleanup` | Clean up expired sessions (cron-triggered) |

## iii Primitives Used

- **State** -- session history, cached tool definitions
- **Streams** -- streaming chat responses via `agent:events:{session_id}` group
- **Cron** -- hourly session cleanup
- **HTTP** -- chat, discovery, planning, and session management endpoints

## Prerequisites

- Rust 1.75+
- Running iii engine on `ws://127.0.0.1:49134`
- `ANTHROPIC_API_KEY` environment variable set

## Build

```bash
cargo build --release
```

## Usage

```bash
# Load the key from your secret manager (keychain, 1password, doppler, etc.)
# into the environment before launching the worker — never paste the literal
# key on the command line, since it lands in shell history and `ps` output.
export ANTHROPIC_API_KEY="$(security find-generic-password -s anthropic-api-key -w)"
./target/release/iii-agent --url ws://127.0.0.1:49134 --config ./config.yaml
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

```
Options:
--config <PATH> Path to config.yaml [default: ./config.yaml]
--url <URL> WebSocket URL of the iii engine [default: ws://127.0.0.1:49134]
--manifest Output module manifest as JSON and exit
-h, --help Print help
```

## Configuration

```yaml
anthropic_model: "claude-sonnet-4-20250514" # model to use for chat
max_tokens: 4096 # max tokens per LLM response
max_iterations: 10 # max tool-use loops per message
session_ttl_hours: 24 # session expiry
cron_session_cleanup: "0 0 * * * *" # hourly cleanup schedule
```

## Tests

```bash
cargo test
```
6 changes: 6 additions & 0 deletions agent/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
fn main() {
println!(
"cargo:rustc-env=TARGET={}",
std::env::var("TARGET").unwrap()
);
}
5 changes: 5 additions & 0 deletions agent/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
anthropic_model: "claude-sonnet-4-20250514"
max_tokens: 4096
max_iterations: 10
session_ttl_hours: 24
cron_session_cleanup: "0 0 * * * *"
109 changes: 109 additions & 0 deletions agent/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use anyhow::Result;
use serde::Deserialize;

#[derive(Deserialize, Debug, Clone)]
pub struct AgentConfig {
#[serde(default = "default_model")]
pub anthropic_model: String,
#[serde(default = "default_max_tokens")]
pub max_tokens: u32,
#[serde(default = "default_max_iterations")]
pub max_iterations: u32,
#[serde(default = "default_session_ttl_hours")]
pub session_ttl_hours: u64,
#[serde(default = "default_cron_session_cleanup")]
pub cron_session_cleanup: String,
}

fn default_model() -> String {
"claude-haiku-4-5-20251001".to_string()
}

fn default_max_tokens() -> u32 {
4096
}

fn default_max_iterations() -> u32 {
10
}

fn default_session_ttl_hours() -> u64 {
24
}

fn default_cron_session_cleanup() -> String {
"0 0 * * * *".to_string()
}

impl Default for AgentConfig {
fn default() -> Self {
AgentConfig {
anthropic_model: default_model(),
max_tokens: default_max_tokens(),
max_iterations: default_max_iterations(),
session_ttl_hours: default_session_ttl_hours(),
cron_session_cleanup: default_cron_session_cleanup(),
}
}
}

pub fn load_config(path: &str) -> Result<AgentConfig> {
let contents = std::fs::read_to_string(path)?;
let config: AgentConfig = serde_yaml::from_str(&contents)?;
validate(&config)?;
Ok(config)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

fn validate(cfg: &AgentConfig) -> Result<()> {
if cfg.anthropic_model.trim().is_empty() {
anyhow::bail!("config: anthropic_model must be non-empty");
}
if cfg.max_tokens == 0 {
anyhow::bail!("config: max_tokens must be >= 1");
}
if cfg.max_iterations == 0 {
anyhow::bail!("config: max_iterations must be >= 1");
}
if cfg.session_ttl_hours == 0 {
anyhow::bail!("config: session_ttl_hours must be >= 1");
}
if cfg.cron_session_cleanup.trim().is_empty() {
anyhow::bail!("config: cron_session_cleanup must be non-empty");
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_config_defaults() {
let config = AgentConfig::default();
assert_eq!(config.anthropic_model, "claude-haiku-4-5-20251001");
assert_eq!(config.max_tokens, 4096);
assert_eq!(config.max_iterations, 10);
assert_eq!(config.session_ttl_hours, 24);
}

#[test]
fn test_config_from_yaml() {
let yaml = r#"
anthropic_model: "claude-sonnet-4-20250514"
max_tokens: 8192
max_iterations: 5
"#;
let config: AgentConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.anthropic_model, "claude-sonnet-4-20250514");
assert_eq!(config.max_tokens, 8192);
assert_eq!(config.max_iterations, 5);
assert_eq!(config.session_ttl_hours, 24);
}

#[test]
fn test_config_empty_yaml() {
let config: AgentConfig = serde_yaml::from_str("{}").unwrap();
assert_eq!(config.anthropic_model, "claude-haiku-4-5-20251001");
assert_eq!(config.max_tokens, 4096);
}
}
Loading
Loading