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:
2026-03-22 15:00:43 +00:00
parent 904ffa2d4d
commit 7bf9e25361
4 changed files with 170 additions and 29 deletions

View File

@@ -146,7 +146,7 @@ impl Responder {
messages.push(ChatMessage::new_user_message(&trigger));
}
let tool_defs = ToolRegistry::tool_definitions();
let tool_defs = ToolRegistry::tool_definitions(self.tools.has_gitea());
let model = Model::new(&self.config.mistral.default_model);
let max_iterations = self.config.mistral.max_tool_iterations;
@@ -199,7 +199,17 @@ impl Responder {
.await;
let result_str = match result {
Ok(s) => s,
Ok(s) => {
let preview: String = s.chars().take(500).collect();
info!(
tool = tc.function.name.as_str(),
id = call_id,
result_len = s.len(),
result_preview = preview.as_str(),
"Tool call result"
);
s
}
Err(e) => {
warn!(tool = tc.function.name.as_str(), "Tool failed: {e}");
format!("Error: {e}")
@@ -261,6 +271,7 @@ impl Responder {
trigger_body: &str,
trigger_sender: &str,
room_id: &str,
room_name: &str,
is_dm: bool,
is_spontaneous: bool,
mistral: &Arc<mistralai_client::v1::client::Client>,
@@ -268,6 +279,7 @@ impl Responder {
response_ctx: &ResponseContext,
conversation_registry: &ConversationRegistry,
image_data_uri: Option<&str>,
context_hint: Option<String>,
) -> Option<String> {
// Apply response delay
if !self.config.behavior.instant_responses {
@@ -287,20 +299,46 @@ impl Responder {
let _ = room.typing_notice(true).await;
// Build the input message (with sender prefix for group rooms)
let input_text = if is_dm {
// Pre-response memory query (same as legacy path)
let memory_notes = self.load_memory_notes(response_ctx, trigger_body).await;
// Build the input message with dynamic context header.
// Agent instructions are static (set at creation), so per-message context
// (timestamps, room, members, memory) is prepended to each user message.
let now = chrono::Utc::now();
let epoch_ms = now.timestamp_millis();
let ts_1h = (now - chrono::Duration::hours(1)).timestamp_millis();
let ts_yesterday = (now - chrono::Duration::days(1)).timestamp_millis();
let ts_last_week = (now - chrono::Duration::days(7)).timestamp_millis();
let mut context_header = format!(
"[context: date={}, epoch_ms={}, ts_1h_ago={}, ts_yesterday={}, ts_last_week={}, room={}, room_name={}]",
now.format("%Y-%m-%d"),
epoch_ms,
ts_1h,
ts_yesterday,
ts_last_week,
room_id,
room_name,
);
if let Some(ref notes) = memory_notes {
context_header.push('\n');
context_header.push_str(notes);
}
let user_msg = if is_dm {
trigger_body.to_string()
} else {
format!("<{}> {}", response_ctx.matrix_user_id, trigger_body)
};
// TODO: multimodal via image_data_uri — Conversations API may support
// content parts in entries. For now, append image description request.
let input_text = format!("{context_header}\n{user_msg}");
let input = ConversationInput::Text(input_text);
// Send through conversation registry
let response = match conversation_registry
.send_message(room_id, input, is_dm, mistral)
.send_message(room_id, input, is_dm, mistral, context_hint.as_deref())
.await
{
Ok(r) => r,
@@ -346,15 +384,23 @@ impl Responder {
.await;
let result_str = match result {
Ok(s) => s,
Ok(s) => {
let preview: String = s.chars().take(500).collect();
info!(
tool = fc.name.as_str(),
id = call_id,
result_len = s.len(),
result_preview = preview.as_str(),
"Tool call result (conversations)"
);
s
}
Err(e) => {
warn!(tool = fc.name.as_str(), "Tool failed: {e}");
format!("Error: {e}")
}
};
result_entries.push(ConversationEntry::FunctionResult(FunctionResultEntry {
tool_call_id: call_id.to_string(),
result: result_str,