per-message context headers, memory notes, conversation continuity
conversations API path now injects per-message context headers with live timestamps, room name, and memory notes. this replaces the template variables in agent instructions which were frozen at creation time. memory notes (topical + recent backfill) loaded before each response in the conversations path — was previously only in the legacy path. context hint seeds new conversations with recent room history after resets, so sol doesn't lose conversational continuity on sneeze. tool call results now logged with preview + length for debugging. reset_all() clears both in-memory and sqlite conversation state.
This commit is contained in:
77
src/main.rs
77
src/main.rs
@@ -8,6 +8,7 @@ mod conversations;
|
||||
mod matrix_utils;
|
||||
mod memory;
|
||||
mod persistence;
|
||||
mod sdk;
|
||||
mod sync;
|
||||
mod tools;
|
||||
|
||||
@@ -65,6 +66,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
let mistral_api_key = std::env::var("SOL_MISTRAL_API_KEY")
|
||||
.map_err(|_| anyhow::anyhow!("SOL_MISTRAL_API_KEY not set"))?;
|
||||
|
||||
// Optional Gitea admin credentials for user impersonation
|
||||
let gitea_admin_username = std::env::var("SOL_GITEA_ADMIN_USERNAME").ok();
|
||||
let gitea_admin_password = std::env::var("SOL_GITEA_ADMIN_PASSWORD").ok();
|
||||
|
||||
let config = Arc::new(config);
|
||||
|
||||
// Initialize Matrix client with E2EE and sqlite store
|
||||
@@ -131,13 +136,52 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize persistent state database (needed by token store + agent/conversation registries)
|
||||
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 Vault client for secure token storage
|
||||
let vault_client = Arc::new(sdk::vault::VaultClient::new(
|
||||
&config.vault.url,
|
||||
&config.vault.role,
|
||||
&config.vault.mount,
|
||||
));
|
||||
|
||||
// Initialize Gitea client if configured
|
||||
let gitea_client: Option<Arc<sdk::gitea::GiteaClient>> =
|
||||
if let (Some(gitea_config), Some(admin_user), Some(admin_pass)) =
|
||||
(&config.services.gitea, &gitea_admin_username, &gitea_admin_password)
|
||||
{
|
||||
let token_store = Arc::new(sdk::tokens::TokenStore::new(
|
||||
store.clone(),
|
||||
vault_client.clone(),
|
||||
));
|
||||
info!(url = gitea_config.url.as_str(), "Gitea integration enabled");
|
||||
Some(Arc::new(sdk::gitea::GiteaClient::new(
|
||||
gitea_config.url.clone(),
|
||||
admin_user.clone(),
|
||||
admin_pass.clone(),
|
||||
token_store,
|
||||
)))
|
||||
} else {
|
||||
info!("Gitea integration disabled (missing config or credentials)");
|
||||
None
|
||||
};
|
||||
|
||||
let tool_registry = Arc::new(ToolRegistry::new(
|
||||
os_client.clone(),
|
||||
matrix_client.clone(),
|
||||
config.clone(),
|
||||
gitea_client,
|
||||
));
|
||||
let indexer = Arc::new(Indexer::new(os_client.clone(), config.clone()));
|
||||
let evaluator = Arc::new(Evaluator::new(config.clone()));
|
||||
let evaluator = Arc::new(Evaluator::new(config.clone(), system_prompt_text.clone()));
|
||||
let responder = Arc::new(Responder::new(
|
||||
config.clone(),
|
||||
personality,
|
||||
@@ -148,22 +192,12 @@ 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,
|
||||
store.clone(),
|
||||
));
|
||||
|
||||
// Build shared state
|
||||
@@ -182,9 +216,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
});
|
||||
|
||||
// Initialize orchestrator agent if conversations API is enabled
|
||||
let mut agent_recreated = false;
|
||||
if config.agents.use_conversations_api {
|
||||
info!("Conversations API enabled — ensuring orchestrator agent exists");
|
||||
let agent_tools = tools::ToolRegistry::agent_tool_definitions();
|
||||
let agent_tools = tools::ToolRegistry::agent_tool_definitions(config.services.gitea.is_some());
|
||||
match state
|
||||
.agent_registry
|
||||
.ensure_orchestrator(
|
||||
@@ -192,12 +227,20 @@ async fn main() -> anyhow::Result<()> {
|
||||
&config.agents.orchestrator_model,
|
||||
agent_tools,
|
||||
&state.mistral,
|
||||
&[], // no domain agents yet — delegation section added when they are
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(agent_id) => {
|
||||
info!(agent_id = agent_id.as_str(), "Orchestrator agent ready");
|
||||
Ok((agent_id, recreated)) => {
|
||||
info!(agent_id = agent_id.as_str(), recreated, "Orchestrator agent ready");
|
||||
state.conversation_registry.set_agent_id(agent_id).await;
|
||||
|
||||
if recreated {
|
||||
// Agent was recreated (system prompt changed) — old conversations
|
||||
// are bound to the stale agent and won't work. Reset everything.
|
||||
state.conversation_registry.reset_all().await;
|
||||
agent_recreated = true;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to create orchestrator agent: {e}");
|
||||
@@ -221,8 +264,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
});
|
||||
|
||||
// If state recovery failed, sneeze into all rooms to signal the hiccup
|
||||
if state_recovery_failed {
|
||||
// If state recovery failed or agent was recreated, sneeze into all rooms
|
||||
if state_recovery_failed || agent_recreated {
|
||||
info!("State recovery failed — sneezing into all rooms");
|
||||
for room in matrix_client.joined_rooms() {
|
||||
let content = ruma::events::room::message::RoomMessageEventContent::text_plain(
|
||||
|
||||
Reference in New Issue
Block a user