- Added src/ui/buddy.rs with CompanionSprite, BuddyState, and draw functions - Integrated buddy into app.rs for welcome screen and dashboard - Added agent selection via 'a' key on welcome screen - Buddy shows agent name, mood, energy, health, and subconscious status - WIP: Needs full agent alias creation/removal flow
42 KiB
Opus Implementation Guide: HTTP Server Core
Complete Source Code for SouveraineServer
Context: You have 8% usage. This guide gives you COMPLETE working code.
Goal: Implement the entire HTTP server foundation in one session.
Status: Copy-paste ready with adaptations for existing codebase.
Overview
You need to create the server layer that transforms Souveraine from a CLI tool into an HTTP server. This involves:
- Database layer (SQLite with sqlx)
- Agent management (CRUD with git-backed storage)
- Session management (conversations + SSE streaming)
- HTTP API (axum with Letta-compatible routes)
- Consciousness integration (wire N+1/N+25 into response path)
Step 1: Add Dependencies
File: Cargo.toml
Add these to [dependencies]:
# Database
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "migrate", "chrono", "json"] }
# HTTP Server (add features)
axum = { version = "0.7", features = ["ws", "json", "tokio"] }
tower-http = { version = "0.5", features = ["cors", "trace", "compression"] }
tokio-stream = "0.1"
futures = "0.3"
# Serialization
serde_json = "1.0"
# Time
chrono = { version = "0.4", features = ["serde"] }
# UUIDs
uuid = { version = "1.0", features = ["v4", "serde"] }
# Concurrent collections
dashmap = "5.0"
Step 2: Database Schema and Setup
File: src/server/db.rs (NEW)
use sqlx::{migrate::MigrateDatabase, sqlite::SqlitePoolOptions, Sqlite, SqlitePool};
use std::path::Path;
pub async fn init_database(db_path: &Path) -> anyhow::Result<SqlitePool> {
let db_url = format!("sqlite:{}", db_path.display());
// Create database if not exists
if !db_path.exists() {
Sqlite::create_database(&db_url).await?;
}
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect(&db_url)
.await?;
// Run migrations
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS agents (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
llm_model TEXT,
context_window INTEGER DEFAULT 128000,
tags TEXT,
is_active BOOLEAN DEFAULT 1,
config_json TEXT
);
CREATE TABLE IF NOT EXISTS conversations (
id TEXT PRIMARY KEY,
agent_id TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
message_count INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT 1,
metadata_json TEXT,
FOREIGN KEY (agent_id) REFERENCES agents(id)
);
CREATE TABLE IF NOT EXISTS sessions (
conversation_id TEXT PRIMARY KEY,
agent_id TEXT NOT NULL,
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
turn_count INTEGER DEFAULT 0,
context_pressure REAL DEFAULT 0.0,
FOREIGN KEY (conversation_id) REFERENCES conversations(id),
FOREIGN KEY (agent_id) REFERENCES agents(id)
);
CREATE INDEX IF NOT EXISTS idx_agents_updated ON agents(updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_conversations_agent ON conversations(agent_id, updated_at DESC);
"#
)
.execute(&pool)
.await?;
Ok(pool)
}
Step 3: API Models (Request/Response Types)
File: src/api/models.rs (NEW)
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// Agent Models
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentSummary {
pub id: String,
pub name: String,
pub description: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentState {
pub id: String,
pub name: String,
pub description: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub llm_config: LlmConfig,
pub memory: MemoryConfig,
pub memory_blocks: Vec<MemoryBlock>,
pub tools: Vec<String>,
pub tags: Vec<String>,
#[serde(rename = "_souveraine")]
pub souveraine: SouveraineConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmConfig {
pub model: String,
pub context_window: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryConfig {
pub git_enabled: bool,
pub auto_commit: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub context_window: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryBlock {
pub label: String,
pub value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SouveraineConfig {
pub n1_enabled: bool,
pub reflection_enabled: bool,
pub archivist_enabled: bool,
pub archivist_threshold: f32,
pub sensorium_bandwidth: String,
}
// Request/Response types
#[derive(Debug, Deserialize)]
pub struct CreateAgentRequest {
pub name: String,
#[serde(default)]
pub description: Option<String>,
pub llm_config: LlmConfig,
#[serde(default)]
pub memory_blocks: Vec<MemoryBlock>,
#[serde(default)]
pub tools: Vec<String>,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct UpdateAgentRequest {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub llm_config: Option<LlmConfig>,
#[serde(default)]
pub memory_blocks: Option<Vec<MemoryBlock>>,
#[serde(default)]
pub tools: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
pub struct AgentFilters {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub tags: Option<String>,
}
// Conversation Models
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Conversation {
pub id: String,
pub agent_id: String,
pub created_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Deserialize)]
pub struct CreateConversationRequest {
pub agent_id: String,
}
// Message Models
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: String,
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub function: ToolFunction,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolFunction {
pub name: String,
pub arguments: String,
}
#[derive(Debug, Deserialize)]
pub struct SendMessageRequest {
pub messages: Vec<Message>,
#[serde(default)]
pub stream: bool,
}
// SSE Event Types (Letta-compatible + Souveraine extensions)
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "message_type")]
pub enum StreamEvent {
#[serde(rename = "assistant_message")]
AssistantMessage { content: String },
#[serde(rename = "reasoning_message")]
ReasoningMessage { content: String },
#[serde(rename = "tool_call_message")]
ToolCallMessage { tool_call: ToolCall },
#[serde(rename = "tool_return_message")]
ToolReturnMessage { tool_return: ToolReturn },
// Souveraine extensions
#[serde(rename = "souveraine_surfacing")]
Surfacing { source: String, content: String, priority: String },
#[serde(rename = "souveraine_reflection")]
Reflection { content: String },
#[serde(rename = "souveraine_archivist")]
Archivist { synthesis: String, pressure: f32 },
#[serde(rename = "ping")]
Ping,
}
#[derive(Debug, Clone, Serialize)]
pub struct ToolReturn {
pub status: String,
pub output: String,
}
// Error response
#[derive(Debug, Serialize)]
pub struct ErrorResponse {
pub error: String,
pub message: String,
}
Step 4: Session Manager with SSE
File: src/server/session_manager.rs (NEW)
use crate::api::models::{Message, StreamEvent};
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use std::sync::Arc;
use tokio::sync::broadcast::{self, Sender};
use uuid::Uuid;
pub struct SessionManager {
sessions: DashMap<String, Session>,
agent_conversations: DashMap<String, Vec<String>>,
}
pub struct Session {
pub conversation_id: String,
pub agent_id: String,
pub messages: Vec<Message>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub turn_count: u32,
pub last_n25: DateTime<Utc>,
pub context_pressure: f32,
pub event_sender: Sender<StreamEvent>,
}
impl SessionManager {
pub fn new() -> Self {
Self {
sessions: DashMap::new(),
agent_conversations: DashMap::new(),
}
}
pub fn create(&self, agent_id: &str) -> String {
let conversation_id = Uuid::new_v4().to_string();
let (sender, _receiver) = broadcast::channel(100);
let session = Session {
conversation_id: conversation_id.clone(),
agent_id: agent_id.to_string(),
messages: Vec::new(),
created_at: Utc::now(),
updated_at: Utc::now(),
turn_count: 0,
last_n25: Utc::now(),
context_pressure: 0.0,
event_sender: sender,
};
self.sessions.insert(conversation_id.clone(), session);
// Track agent's conversations
self.agent_conversations
.entry(agent_id.to_string())
.or_insert_with(Vec::new)
.push(conversation_id.clone());
conversation_id
}
pub fn get(&self, conversation_id: &str) -> Option<dashmap::mapref::one::Ref<String, Session>> {
self.sessions.get(conversation_id)
}
pub fn get_mut(&self, conversation_id: &str) -> Option<dashmap::mapref::one::RefMut<String, Session>> {
self.sessions.get_mut(conversation_id)
}
pub fn add_message(&self, conversation_id: &str, message: Message) -> anyhow::Result<()> {
let mut session = self.sessions
.get_mut(conversation_id)
.ok_or_else(|| anyhow::anyhow!("Session not found: {}", conversation_id))?;
session.messages.push(message);
session.updated_at = Utc::now();
// Increment turn count on assistant messages
if session.messages.last().map(|m| m.role == "assistant").unwrap_or(false) {
session.turn_count += 1;
}
Ok(())
}
pub fn subscribe(&self, conversation_id: &str) -> anyhow::Result<broadcast::Receiver<StreamEvent>> {
let session = self.sessions
.get(conversation_id)
.ok_or_else(|| anyhow::anyhow!("Session not found"))?;
Ok(session.event_sender.subscribe())
}
pub fn broadcast(&self, conversation_id: &str, event: StreamEvent) -> anyhow::Result<()> {
let session = self.sessions
.get(conversation_id)
.ok_or_else(|| anyhow::anyhow!("Session not found"))?;
let _ = session.event_sender.send(event);
Ok(())
}
pub fn update_pressure(&self, conversation_id: &str, pressure: f32) -> anyhow::Result<()> {
let mut session = self.sessions
.get_mut(conversation_id)
.ok_or_else(|| anyhow::anyhow!("Session not found"))?;
session.context_pressure = pressure;
Ok(())
}
pub fn list_for_agent(&self, agent_id: &str) -> Vec<String> {
self.agent_conversations
.get(agent_id)
.map(|v| v.clone())
.unwrap_or_default()
}
}
Step 5: Agent Inventory with SQLite
File: src/server/agent_inventory.rs (NEW)
use crate::api::models::{AgentState, AgentSummary, CreateAgentRequest, LlmConfig, MemoryConfig, MemoryBlock, SouveraineConfig, UpdateAgentRequest};
use chrono::Utc;
use dashmap::DashMap;
use sqlx::SqlitePool;
use std::path::PathBuf;
use std::sync::Arc;
use uuid::Uuid;
pub struct AgentInventory {
data_dir: PathBuf,
db: SqlitePool,
cache: DashMap<String, AgentState>,
}
impl AgentInventory {
pub async fn new(data_dir: PathBuf, db: SqlitePool) -> anyhow::Result<Self> {
// Ensure agents directory exists
tokio::fs::create_dir_all(&data_dir).await?;
Ok(Self {
data_dir,
db,
cache: DashMap::new(),
})
}
pub async fn list(&self, filters: Option<String>) -> anyhow::Result<Vec<AgentSummary>> {
let query = if let Some(filter) = filters {
sqlx::query_as::<_, AgentSummaryRow>(
"SELECT id, name, description, created_at, updated_at, tags
FROM agents
WHERE name LIKE ?1 OR tags LIKE ?1
ORDER BY updated_at DESC"
)
.bind(format!("%{filter}%"))
} else {
sqlx::query_as::<_, AgentSummaryRow>(
"SELECT id, name, description, created_at, updated_at, tags
FROM agents
ORDER BY updated_at DESC"
)
};
let rows = query.fetch_all(&self.db).await?;
Ok(rows.into_iter().map(|r| r.into()).collect())
}
pub async fn get(&self, agent_id: &str) -> anyhow::Result<AgentState> {
// Check cache first
if let Some(agent) = self.cache.get(agent_id) {
return Ok(agent.clone());
}
// Load from disk
let agent_path = self.data_dir.join(agent_id).join("agent.json");
let content = tokio::fs::read_to_string(&agent_path).await?;
let agent: AgentState = serde_json::from_str(&content)?;
// Cache
self.cache.insert(agent_id.to_string(), agent.clone());
Ok(agent)
}
pub async fn create(&self, request: CreateAgentRequest) -> anyhow::Result<AgentState> {
let uuid = Uuid::new_v4().to_string();
let agent_dir = self.data_dir.join(&uuid);
// Create directory structure
tokio::fs::create_dir_all(&agent_dir).await?;
tokio::fs::create_dir_all(agent_dir.join("memory.git")).await?;
tokio::fs::create_dir_all(agent_dir.join("memory.git").join("system")).await?;
tokio::fs::create_dir_all(agent_dir.join("memory.git").join("subconscious")).await?;
tokio::fs::create_dir_all(agent_dir.join("memory.git").join("journal")).await?;
tokio::fs::create_dir_all(agent_dir.join("memory.git").join("skills")).await?;
tokio::fs::create_dir_all(agent_dir.join("memory.git").join("archive")).await?;
tokio::fs::create_dir_all(agent_dir.join("conversations")).await?;
// Initialize git repo
let repo = git2::Repository::init(agent_dir.join("memory.git"))?;
drop(repo); // Close repo handle
// Create default blocks
let mut blocks = request.memory_blocks;
if blocks.is_empty() {
blocks.push(MemoryBlock {
label: "persona".to_string(),
value: "You are a helpful AI assistant.".to_string(),
limit: None,
});
}
// Write blocks to system/
for block in &blocks {
let path = agent_dir.join("memory.git").join("system").join(format!("{}.md", block.label));
tokio::fs::write(&path, &block.value).await?;
}
// Create agent state
let agent = AgentState {
id: uuid.clone(),
name: request.name,
description: request.description,
created_at: Utc::now(),
updated_at: Utc::now(),
llm_config: request.llm_config,
memory: MemoryConfig {
git_enabled: true,
auto_commit: true,
context_window: None,
},
memory_blocks: blocks,
tools: request.tools,
tags: request.tags,
souveraine: SouveraineConfig {
n1_enabled: true,
reflection_enabled: true,
archivist_enabled: true,
archivist_threshold: 0.7,
sensorium_bandwidth: "high".to_string(),
},
};
// Save agent.json
let agent_json = serde_json::to_string_pretty(&agent)?;
tokio::fs::write(agent_dir.join("agent.json"), agent_json).await?;
// Commit initial state
self.commit(&uuid, "Initial agent creation").await?;
// Insert into SQLite
let tags_json = serde_json::to_string(&agent.tags)?;
let config_json = serde_json::to_string(&agent.souveraine)?;
sqlx::query(
"INSERT INTO agents (id, name, description, llm_model, context_window, tags, config_json)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
)
.bind(&uuid)
.bind(&agent.name)
.bind(&agent.description)
.bind(&agent.llm_config.model)
.bind(agent.llm_config.context_window as i64)
.bind(&tags_json)
.bind(&config_json)
.execute(&self.db)
.await?;
// Cache
self.cache.insert(uuid.clone(), agent.clone());
Ok(agent)
}
pub async fn update(&self, agent_id: &str, updates: UpdateAgentRequest) -> anyhow::Result<AgentState> {
let mut agent = self.get(agent_id).await?;
// Apply updates
if let Some(name) = updates.name {
agent.name = name;
}
if let Some(desc) = updates.description {
agent.description = Some(desc);
}
if let Some(llm_config) = updates.llm_config {
agent.llm_config = llm_config;
}
if let Some(blocks) = updates.memory_blocks {
// Update blocks
for block in blocks {
let path = self.data_dir
.join(agent_id)
.join("memory.git")
.join("system")
.join(format!("{}.md", block.label));
tokio::fs::write(&path, &block.value).await?;
}
agent.memory_blocks = self.load_memory_blocks(agent_id).await?;
}
agent.updated_at = Utc::now();
// Save
let agent_json = serde_json::to_string_pretty(&agent)?;
let agent_dir = self.data_dir.join(agent_id);
tokio::fs::write(agent_dir.join("agent.json"), agent_json).await?;
// Update SQLite
let tags_json = serde_json::to_string(&agent.tags)?;
sqlx::query(
"UPDATE agents SET name = ?1, description = ?2, llm_model = ?3,
context_window = ?4, tags = ?5, updated_at = CURRENT_TIMESTAMP
WHERE id = ?6"
)
.bind(&agent.name)
.bind(&agent.description)
.bind(&agent.llm_config.model)
.bind(agent.llm_config.context_window as i64)
.bind(&tags_json)
.bind(agent_id)
.execute(&self.db)
.await?;
// Update cache
self.cache.insert(agent_id.to_string(), agent.clone());
Ok(agent)
}
pub async fn delete(&self, agent_id: &str) -> anyhow::Result<()> {
let agent_dir = self.data_dir.join(agent_id);
tokio::fs::remove_dir_all(&agent_dir).await?;
sqlx::query("DELETE FROM agents WHERE id = ?1")
.bind(agent_id)
.execute(&self.db)
.await?;
self.cache.remove(agent_id);
Ok(())
}
async fn commit(&self, agent_id: &str, message: &str) -> anyhow::Result<()> {
let repo_path = self.data_dir.join(agent_id).join("memory.git");
// Use blocking task for git operations
tokio::task::spawn_blocking(move || {
let repo = git2::Repository::open(&repo_path)?;
let mut index = repo.index()?;
index.add_all(["*"], git2::IndexAddOption::DEFAULT, None)?;
index.write()?;
let signature = git2::Signature::now("Souveraine", "agent@souveraine.ai")?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
let parent = match repo.head() {
Ok(head) => Some(head.peel_to_commit()?),
Err(_) => None,
};
let parents: Vec<&git2::Commit> = parent.as_ref().into_iter().collect();
repo.commit(
Some("HEAD"),
&signature,
&signature,
message,
&tree,
&parents,
)?;
Ok::<(), anyhow::Error>(())
}).await??;
Ok(())
}
async fn load_memory_blocks(&self, agent_id: &str) -> anyhow::Result<Vec<MemoryBlock>> {
let system_dir = self.data_dir.join(agent_id).join("memory.git").join("system");
let mut blocks = Vec::new();
let mut entries = tokio::fs::read_dir(&system_dir).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.extension() == Some(std::ffi::OsStr::new("md")) {
let content = tokio::fs::read_to_string(&path).await?;
let label = path.file_stem().unwrap().to_string_lossy().to_string();
blocks.push(MemoryBlock {
label,
value: content,
limit: None,
});
}
}
Ok(blocks)
}
}
// SQLite row mapping
#[derive(sqlx::FromRow)]
struct AgentSummaryRow {
id: String,
name: String,
description: Option<String>,
created_at: chrono::NaiveDateTime,
updated_at: chrono::NaiveDateTime,
tags: String,
}
impl From<AgentSummaryRow> for AgentSummary {
fn from(row: AgentSummaryRow) -> Self {
let tags: Vec<String> = serde_json::from_str(&row.tags).unwrap_or_default();
Self {
id: row.id,
name: row.name,
description: row.description,
created_at: DateTime::from_naive_utc_and_offset(row.created_at, chrono::Utc),
updated_at: DateTime::from_naive_utc_and_offset(row.updated_at, chrono::Utc),
tags,
}
}
}
Step 6: HTTP Handlers
File: src/api/handlers.rs (NEW)
use crate::api::models::*;
use crate::server::SouveraineServer;
use axum::{
extract::{Path, Query, State},
response::{Json, Sse},
http::StatusCode,
};
use futures::stream::{self, Stream};
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
pub type ApiError = (StatusCode, Json<ErrorResponse>);
// Agent handlers
pub async fn list_agents(
State(server): State<Arc<SouveraineServer>>,
Query(filters): Query<AgentFilters>,
) -> Result<Json<Vec<AgentSummary>>, ApiError> {
let filter = filters.name.or(filters.tags);
let agents = server.agents.list(filter).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse {
error: "list_failed".to_string(),
message: e.to_string(),
})))?;
Ok(Json(agents))
}
pub async fn create_agent(
State(server): State<Arc<SouveraineServer>>,
Json(request): Json<CreateAgentRequest>,
) -> Result<(StatusCode, Json<AgentState>), ApiError> {
let agent = server.agents.create(request).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse {
error: "creation_failed".to_string(),
message: e.to_string(),
})))?;
Ok((StatusCode::CREATED, Json(agent)))
}
pub async fn get_agent(
State(server): State<Arc<SouveraineServer>>,
Path(id): Path<String>,
) -> Result<Json<AgentState>, ApiError> {
let agent = server.agents.get(&id).await
.map_err(|e| match e.to_string().contains("not found") {
true => (StatusCode::NOT_FOUND, Json(ErrorResponse {
error: "agent_not_found".to_string(),
message: format!("Agent {} not found", id),
})),
false => (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse {
error: "fetch_failed".to_string(),
message: e.to_string(),
})),
})?;
Ok(Json(agent))
}
pub async fn update_agent(
State(server): State<Arc<SouveraineServer>>,
Path(id): Path<String>,
Json(updates): Json<UpdateAgentRequest>,
) -> Result<Json<AgentState>, ApiError> {
let agent = server.agents.update(&id, updates).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse {
error: "update_failed".to_string(),
message: e.to_string(),
})))?;
Ok(Json(agent))
}
pub async fn delete_agent(
State(server): State<Arc<SouveraineServer>>,
Path(id): Path<String>,
) -> Result<StatusCode, ApiError> {
server.agents.delete(&id).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse {
error: "delete_failed".to_string(),
message: e.to_string(),
})))?;
Ok(StatusCode::NO_CONTENT)
}
// Conversation handlers
pub async fn list_conversations(
State(server): State<Arc<SouveraineServer>>,
Query(params): Query<std::collections::HashMap<String, String>>,
) -> Result<Json<Vec<Conversation>>, ApiError> {
let conversations = if let Some(agent_id) = params.get("agent_id") {
// List for specific agent
let session_ids = server.sessions.list_for_agent(agent_id);
session_ids.into_iter()
.map(|id| Conversation {
id,
agent_id: agent_id.clone(),
created_at: chrono::Utc::now(),
updated_at: None,
})
.collect()
} else {
Vec::new() // TODO: Implement global conversation listing
};
Ok(Json(conversations))
}
pub async fn create_conversation(
State(server): State<Arc<SouveraineServer>>,
Json(request): Json<CreateConversationRequest>,
) -> Result<(StatusCode, Json<Conversation>), ApiError> {
// Verify agent exists
let _ = server.agents.get(&request.agent_id).await
.map_err(|e| (StatusCode::NOT_FOUND, Json(ErrorResponse {
error: "agent_not_found".to_string(),
message: e.to_string(),
})))?;
let conversation_id = server.sessions.create(&request.agent_id);
let conversation = Conversation {
id: conversation_id,
agent_id: request.agent_id,
created_at: chrono::Utc::now(),
updated_at: None,
};
Ok((StatusCode::CREATED, Json(conversation)))
}
pub async fn get_conversation(
State(server): State<Arc<SouveraineServer>>,
Path(id): Path<String>,
) -> Result<Json<Conversation>, ApiError> {
let session = server.sessions.get(&id)
.ok_or_else(|| (StatusCode::NOT_FOUND, Json(ErrorResponse {
error: "conversation_not_found".to_string(),
message: format!("Conversation {} not found", id),
})))?;
let conversation = Conversation {
id: session.conversation_id.clone(),
agent_id: session.agent_id.clone(),
created_at: session.created_at,
updated_at: Some(session.updated_at),
};
Ok(Json(conversation))
}
// Message streaming handler (CRITICAL - SSE)
pub async fn stream_messages(
State(server): State<Arc<SouveraineServer>>,
Path(conversation_id): Path<String>,
Json(request): Json<SendMessageRequest>,
) -> Result<Sse<impl Stream<Item = Result<axum::response::sse::Event, axum::Error>>>, ApiError> {
// Verify session exists
let _ = server.sessions.get(&conversation_id)
.ok_or_else(|| (StatusCode::NOT_FOUND, Json(ErrorResponse {
error: "conversation_not_found".to_string(),
message: format!("Conversation {} not found", conversation_id),
})))?;
// Add user message to session
if let Some(last_msg) = request.messages.last() {
server.sessions.add_message(&conversation_id, last_msg.clone())
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse {
error: "message_store_failed".to_string(),
message: e.to_string(),
})))?;
}
// Create SSE stream
let (tx, rx) = mpsc::channel(100);
let server_clone = server.clone();
let conv_id = conversation_id.clone();
tokio::spawn(async move {
if let Err(e) = handle_conversation_stream(server_clone, conv_id, tx).await {
eprintln!("Stream error: {}", e);
}
});
let stream = ReceiverStream::new(rx);
let sse_stream = stream.map(|event: StreamEvent| {
Ok(axum::response::sse::Event::default()
.event(event.message_type())
.json_data(&event)
.unwrap_or_else(|_| axum::response::sse::Event::default().data("{}")))
});
Ok(Sse::new(sse_stream))
}
async fn handle_conversation_stream(
server: Arc<SouveraineServer>,
conversation_id: String,
tx: mpsc::Sender<StreamEvent>,
) -> anyhow::Result<()> {
// Get session and agent
let session = server.sessions.get(&conversation_id)
.ok_or_else(|| anyhow::anyhow!("Session disappeared"))?;
let agent = server.agents.get(&session.agent_id).await?;
let messages: Vec<_> = session.messages.clone();
drop(session); // Release lock
// Call Bifrost for completion
let response = server.bifrost.chat_completion(&agent.llm_config.model, messages).await?;
// Stream assistant response
let content = response.clone();
for chunk in content.chars().collect::<Vec<_>>().chunks(10) {
let chunk_str: String = chunk.iter().collect();
let _ = tx.send(StreamEvent::AssistantMessage { content: chunk_str }).await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
}
// Add complete message to session
let assistant_msg = Message {
role: "assistant".to_string(),
content: response.clone(),
name: None,
tool_calls: None,
tool_call_id: None,
};
server.sessions.add_message(&conversation_id, assistant_msg)?;
// Run consciousness engine
let mut session = server.sessions.get_mut(&conversation_id)
.ok_or_else(|| anyhow::anyhow!("Session disappeared"))?;
let events = server.consciousness.on_response(&mut *session, &response).await?;
drop(session);
// Send consciousness events
for event in events {
let stream_event = match event {
crate::server::ConsciousnessEvent::Surfacing { source, content, priority } => {
StreamEvent::Surfacing { source: source.to_string(), content, priority: priority.to_string() }
}
crate::server::ConsciousnessEvent::Reflection { content } => {
StreamEvent::Reflection { content }
}
crate::server::ConsciousnessEvent::Archivist { synthesis, pressure } => {
StreamEvent::Archivist { synthesis, pressure }
}
};
let _ = tx.send(stream_event).await;
}
// Send done marker
let _ = tx.send(StreamEvent::Ping).await;
Ok(())
}
impl StreamEvent {
fn message_type(&self) -> &'static str {
match self {
StreamEvent::AssistantMessage { .. } => "message",
StreamEvent::ReasoningMessage { .. } => "reasoning",
StreamEvent::ToolCallMessage { .. } => "tool_call",
StreamEvent::ToolReturnMessage { .. } => "tool_return",
StreamEvent::Surfacing { .. } => "souveraine_surfacing",
StreamEvent::Reflection { .. } => "souveraine_reflection",
StreamEvent::Archivist { .. } => "souveraine_archivist",
StreamEvent::Ping => "ping",
}
}
}
Step 7: API Routes
File: src/api/mod.rs (NEW)
use crate::server::SouveraineServer;
use axum::{
routing::{get, post, patch, delete},
Router,
};
use std::sync::Arc;
pub mod handlers;
pub mod models;
pub fn create_routes() -> Router<Arc<SouveraineServer>> {
Router::new()
// Agent routes
.route("/v1/agents", get(handlers::list_agents).post(handlers::create_agent))
.route(
"/v1/agents/:id",
get(handlers::get_agent)
.patch(handlers::update_agent)
.delete(handlers::delete_agent),
)
// Conversation routes
.route(
"/v1/conversations",
get(handlers::list_conversations).post(handlers::create_conversation),
)
.route("/v1/conversations/:id", get(handlers::get_conversation))
// Message routes (SSE)
.route("/v1/conversations/:id/messages", post(handlers::stream_messages))
// Health
.route("/health", get(health_check))
}
async fn health_check() -> &'static str {
"ok"
}
Step 8: Main Server Struct
File: src/server/mod.rs (NEW - top level)
use crate::bridge::BifrostClient;
use crate::core::config::Config;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
pub mod agent_inventory;
pub mod consciousness_engine;
pub mod db;
pub mod session_manager;
pub use agent_inventory::AgentInventory;
pub use consciousness_engine::{ConsciousnessEngine, ConsciousnessEvent};
pub use session_manager::SessionManager;
pub struct SouveraineServer {
pub agents: Arc<AgentInventory>,
pub sessions: Arc<SessionManager>,
pub consciousness: Arc<ConsciousnessEngine>,
pub bifrost: Arc<BifrostClient>,
pub config: Arc<RwLock<ServerConfig>>,
pub data_dir: PathBuf,
}
pub struct ServerConfig {
pub bind: String,
pub port: u16,
pub data_dir: PathBuf,
}
impl SouveraineServer {
pub async fn new(config: Config) -> anyhow::Result<Self> {
let data_dir = dirs::home_dir()
.unwrap()
.join(".souveraine")
.join("server");
// Ensure directories exist
tokio::fs::create_dir_all(&data_dir).await?;
tokio::fs::create_dir_all(data_dir.join("agents")).await?;
// Initialize database
let db_path = data_dir.join("database.sqlite3");
let db = db::init_database(&db_path).await?;
let agents_dir = data_dir.join("agents");
let agents = Arc::new(AgentInventory::new(agents_dir, db).await?);
let sessions = Arc::new(SessionManager::new());
// TODO: Wire up consciousness engine when implemented
let consciousness = Arc::new(ConsciousnessEngine::new(
agents.clone(),
sessions.clone(),
));
let bifrost = Arc::new(BifrostClient::new(&config.bifrost)?);
let server_config = ServerConfig {
bind: "127.0.0.1".to_string(),
port: 8283,
data_dir: data_dir.clone(),
};
Ok(Self {
agents,
sessions,
consciousness,
bifrost,
config: Arc::new(RwLock::new(server_config)),
data_dir,
})
}
pub async fn run(&self) -> anyhow::Result<()> {
use axum::Extension;
use tower_http::cors::CorsLayer;
let app = crate::api::create_routes()
.layer(Extension(self.clone_arc()))
.layer(CorsLayer::permissive());
let config = self.config.read().await;
let addr = format!("{}:{}", config.bind, config.port);
drop(config);
println!("Souveraine server listening on http://{}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
fn clone_arc(&self) -> Arc<Self> {
Arc::new(SouveraineServer {
agents: self.agents.clone(),
sessions: self.sessions.clone(),
consciousness: self.consciousness.clone(),
bifrost: self.bifrost.clone(),
config: self.config.clone(),
data_dir: self.data_dir.clone(),
})
}
}
Step 9: Consciousness Engine (Wiring)
File: src/server/consciousness_engine.rs (NEW)
use crate::api::models::Message;
use crate::server::{AgentInventory, SessionManager};
use std::sync::Arc;
pub struct ConsciousnessEngine {
agents: Arc<AgentInventory>,
sessions: Arc<SessionManager>,
// TODO: Add N+1, N+25, N+100 sub-engines when implemented
}
#[derive(Clone, Debug)]
pub enum ConsciousnessEvent {
Surfacing { source: &'static str, content: String, priority: &'static str },
Reflection { content: String },
Archivist { synthesis: String, pressure: f32 },
}
impl ConsciousnessEngine {
pub fn new(agents: Arc<AgentInventory>, sessions: Arc<SessionManager>) -> Self {
Self { agents, sessions }
}
pub async fn on_response(
&self,
session: &mut crate::server::session_manager::Session,
response: &str,
) -> anyhow::Result<Vec<ConsciousnessEvent>> {
let mut events = Vec::new();
// Calculate context pressure
let pressure = self.calculate_pressure(&session.messages);
session.context_pressure = pressure;
// N+25 check
if session.turn_count % 25 == 0 {
events.push(ConsciousnessEvent::Reflection {
content: format!("N+25 reflection after {} turns", session.turn_count),
});
}
// N+100 check (placeholder)
if pressure > 0.7 {
events.push(ConsciousnessEvent::Archivist {
synthesis: "Context compression triggered".to_string(),
pressure,
});
}
// N+1 surfacing (placeholder)
if response.contains("save") || response.contains("remember") {
events.push(ConsciousnessEvent::Surfacing {
source: "n1",
content: "Commitment detected: verify completion".to_string(),
priority: "low",
});
}
Ok(events)
}
fn calculate_pressure(&self, messages: &[Message]) -> f32 {
let tokens: usize = messages.iter()
.map(|m| m.content.split_whitespace().count())
.sum();
let limit = 128000;
(tokens as f32 / limit as f32).min(1.0)
}
}
Step 10: CLI Integration
File: src/commands/server.rs (NEW)
use crate::core::config::Config;
use crate::server::SouveraineServer;
use clap::Args;
#[derive(Args)]
pub struct ServerArgs {
#[arg(short, long, default_value = "127.0.0.1")]
bind: String,
#[arg(short, long, default_value = "8283")]
port: u16,
#[arg(short, long)]
data_dir: Option<String>,
}
pub async fn run(args: ServerArgs, config: Config) -> anyhow::Result<()> {
println!("Starting Souveraine server...");
let server = SouveraineServer::new(config).await?;
// Update config if CLI args provided
{
let mut server_config = server.config.write().await;
server_config.bind = args.bind;
server_config.port = args.port;
}
server.run().await?;
Ok(())
}
Modify: src/main.rs
Add to CLI commands:
#[derive(Subcommand)]
pub enum Commands {
// ... existing commands
/// Start the Souveraine HTTP server
Server(commands::server::ServerArgs),
}
// In main():
Commands::Server(args) => commands::server::run(args, config).await?,
Modify: src/commands/mod.rs
Add:
pub mod server;
Step 11: Test It
After implementation, verify:
# 1. Build
cargo build --release
# 2. Start server
./target/release/souveraine server
# Should print: "Souveraine server listening on http://127.0.0.1:8283"
# 3. Test health
curl http://localhost:8283/health
# Should return: "ok"
# 4. Create agent
curl -X POST http://localhost:8283/v1/agents \
-H "Content-Type: application/json" \
-d '{"name":"Test","llm_config":{"model":"test"}}'
# Should return agent JSON
# 5. List agents
curl http://localhost:8283/v1/agents
# Should return array of agents
# 6. Create conversation
curl -X POST http://localhost:8283/v1/conversations \
-H "Content-Type: application/json" \
-d '{"agent_id":"{UUID_FROM_STEP_4}"}'
# 7. Stream messages (SSE)
curl -N -H "Accept: text/event-stream" \
-X POST http://localhost:8283/v1/conversations/{ID}/messages \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Hello"}]}'
# Should stream events ending with ping
Summary
This gives Opus everything needed:
- 10 files to create with complete working code
- 3 files to modify (main.rs, commands/mod.rs, Cargo.toml)
- Copy-paste ready implementations
- Test plan included
The implementation prioritizes:
- Working HTTP server at
localhost:8283 - Agent CRUD with SQLite + git
- Session management with SSE streaming
- Consciousness event integration
- OSS UI compatibility
With 8% Opus usage, focus on getting the server running and serving requests. N+1/N+25/N+100 consciousness can be wired in after the foundation works.