feat: multi-agent architecture with Conversations API and persistent state
Mistral Agents + Conversations API integration:
- Orchestrator agent created on startup with Sol's personality + tools
- ConversationRegistry routes messages through persistent conversations
- Per-room conversation state (room_id → conversation_id + token counts)
- Function call handling within conversation responses
- Configurable via [agents] section in sol.toml (use_conversations_api flag)
Multimodal support:
- m.image detection and Matrix media download (mxc:// → base64 data URI)
- ContentPart-based messages sent to Mistral vision models
- Archive stores media_urls for image messages
System prompt rewrite:
- 687 → 150 lines — dense, few-shot examples, hard rules
- {room_context_rules} placeholder for group vs DM behavior
- Sender prefixing (<@user:server>) for multi-user turns in group rooms
SQLite persistence (/data/sol.db):
- Conversation mappings and agent IDs survive reboots
- WAL mode for concurrent reads
- Falls back to in-memory on failure (sneezes into all rooms to signal)
- PVC already mounted at /data alongside Matrix SDK state store
New modules:
- src/persistence.rs — SQLite state store
- src/conversations.rs — ConversationRegistry + message merging
- src/agents/{mod,definitions,registry}.rs — agent lifecycle
- src/agent_ux.rs — reaction + thread progress UX
- src/tools/bridge.rs — tool dispatch for domain agents
102 tests passing.
This commit is contained in:
67
src/main.rs
67
src/main.rs
@@ -1,9 +1,13 @@
|
||||
mod agent_ux;
|
||||
mod agents;
|
||||
mod archive;
|
||||
mod brain;
|
||||
mod config;
|
||||
mod context;
|
||||
mod conversations;
|
||||
mod matrix_utils;
|
||||
mod memory;
|
||||
mod persistence;
|
||||
mod sync;
|
||||
mod tools;
|
||||
|
||||
@@ -15,12 +19,14 @@ use opensearch::OpenSearch;
|
||||
use ruma::{OwnedDeviceId, OwnedUserId};
|
||||
use tokio::signal;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{error, info};
|
||||
use tracing::{error, info, warn};
|
||||
use url::Url;
|
||||
|
||||
use agents::registry::AgentRegistry;
|
||||
use archive::indexer::Indexer;
|
||||
use archive::schema::create_index_if_not_exists;
|
||||
use brain::conversation::{ContextMessage, ConversationManager};
|
||||
use conversations::ConversationRegistry;
|
||||
use memory::schema::create_index_if_not_exists as create_memory_index;
|
||||
use brain::evaluator::Evaluator;
|
||||
use brain::personality::Personality;
|
||||
@@ -110,6 +116,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
let mistral = Arc::new(mistral_client);
|
||||
|
||||
// Build components
|
||||
let system_prompt_text = system_prompt.clone();
|
||||
let personality = Arc::new(Personality::new(system_prompt));
|
||||
let conversations = Arc::new(Mutex::new(ConversationManager::new(
|
||||
config.behavior.room_context_window,
|
||||
@@ -141,6 +148,24 @@ async fn main() -> anyhow::Result<()> {
|
||||
// Start background flush task
|
||||
let _flush_handle = indexer.start_flush_task();
|
||||
|
||||
// Initialize persistent state database
|
||||
let (store, state_recovery_failed) = match persistence::Store::open(&config.matrix.db_path) {
|
||||
Ok(s) => (Arc::new(s), false),
|
||||
Err(e) => {
|
||||
error!("Failed to open state database at {}: {e}", config.matrix.db_path);
|
||||
error!("Falling back to in-memory state — conversations will not survive restarts");
|
||||
(Arc::new(persistence::Store::open_memory().expect("in-memory DB must work")), true)
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize agent registry and conversation registry (with SQLite backing)
|
||||
let agent_registry = Arc::new(AgentRegistry::new(store.clone()));
|
||||
let conversation_registry = Arc::new(ConversationRegistry::new(
|
||||
config.mistral.default_model.clone(),
|
||||
config.agents.compaction_threshold,
|
||||
store,
|
||||
));
|
||||
|
||||
// Build shared state
|
||||
let state = Arc::new(AppState {
|
||||
config: config.clone(),
|
||||
@@ -148,12 +173,39 @@ async fn main() -> anyhow::Result<()> {
|
||||
evaluator,
|
||||
responder,
|
||||
conversations,
|
||||
agent_registry,
|
||||
conversation_registry,
|
||||
mistral,
|
||||
opensearch: os_client,
|
||||
last_response: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
|
||||
responding_in: Arc::new(tokio::sync::Mutex::new(std::collections::HashSet::new())),
|
||||
});
|
||||
|
||||
// Initialize orchestrator agent if conversations API is enabled
|
||||
if config.agents.use_conversations_api {
|
||||
info!("Conversations API enabled — ensuring orchestrator agent exists");
|
||||
let agent_tools = tools::ToolRegistry::agent_tool_definitions();
|
||||
match state
|
||||
.agent_registry
|
||||
.ensure_orchestrator(
|
||||
&system_prompt_text,
|
||||
&config.agents.orchestrator_model,
|
||||
agent_tools,
|
||||
&state.mistral,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(agent_id) => {
|
||||
info!(agent_id = agent_id.as_str(), "Orchestrator agent ready");
|
||||
state.conversation_registry.set_agent_id(agent_id).await;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to create orchestrator agent: {e}");
|
||||
error!("Falling back to model-only conversations (no orchestrator)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backfill reactions from Matrix room timelines
|
||||
info!("Backfilling reactions from room timelines...");
|
||||
if let Err(e) = backfill_reactions(&matrix_client, &state.indexer).await {
|
||||
@@ -169,6 +221,19 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
});
|
||||
|
||||
// If state recovery failed, sneeze into all rooms to signal the hiccup
|
||||
if state_recovery_failed {
|
||||
info!("State recovery failed — sneezing into all rooms");
|
||||
for room in matrix_client.joined_rooms() {
|
||||
let content = ruma::events::room::message::RoomMessageEventContent::text_plain(
|
||||
"*sneezes*",
|
||||
);
|
||||
if let Err(e) = room.send(content).await {
|
||||
warn!("Failed to sneeze into {}: {e}", room.room_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Sol is running");
|
||||
|
||||
// Wait for shutdown signal
|
||||
|
||||
Reference in New Issue
Block a user