add TimeContext: 25 pre-computed time values for the model

midnight-based day boundaries (today, yesterday, 2 days ago),
week/month boundaries, rolling offsets (1h to 30d). injected
into system prompt via {time_block} and per-message via compact
time line. models no longer need to compute epoch timestamps.
This commit is contained in:
2026-03-23 01:41:44 +00:00
parent 84278fc1f5
commit 1058afb635
3 changed files with 338 additions and 48 deletions

View File

@@ -21,6 +21,7 @@ use crate::config::Config;
use crate::context::ResponseContext;
use crate::conversations::ConversationRegistry;
use crate::memory;
use crate::time_context::TimeContext;
use crate::tools::ToolRegistry;
/// Run a Mistral chat completion on a blocking thread.
@@ -146,7 +147,7 @@ impl Responder {
messages.push(ChatMessage::new_user_message(&trigger));
}
let tool_defs = ToolRegistry::tool_definitions(self.tools.has_gitea());
let tool_defs = ToolRegistry::tool_definitions(self.tools.has_gitea(), self.tools.has_kratos());
let model = Model::new(&self.config.mistral.default_model);
let max_iterations = self.config.mistral.max_tool_iterations;
@@ -280,6 +281,7 @@ impl Responder {
conversation_registry: &ConversationRegistry,
image_data_uri: Option<&str>,
context_hint: Option<String>,
event_id: ruma::OwnedEventId,
) -> Option<String> {
// Apply response delay
if !self.config.behavior.instant_responses {
@@ -302,24 +304,16 @@ impl Responder {
// 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.
// Build the input message with dynamic context.
// 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 tc = TimeContext::now();
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,
"{}\n[room: {} ({})]",
tc.message_line(),
room_name,
room_id,
);
if let Some(ref notes) = memory_notes {
@@ -352,9 +346,12 @@ impl Responder {
// Check for function calls — execute locally and send results back
let function_calls = response.function_calls();
if !function_calls.is_empty() {
// Agent UX: reactions + threads require the user's event ID
// which we don't have in the responder. For now, log tool calls
// and skip UX. TODO: pass event_id through ResponseContext.
// Agent UX: react with 🔍 and post tool details in a thread
let mut progress = crate::agent_ux::AgentProgress::new(
room.clone(),
event_id.clone(),
);
progress.start().await;
let max_iterations = self.config.mistral.max_tool_iterations;
let mut current_response = response;
@@ -376,13 +373,30 @@ impl Responder {
"Executing tool call (conversations)"
);
let result = self
.tools
.execute(&fc.name, &fc.arguments, response_ctx)
// Post tool call to thread
progress
.post_step(&crate::agent_ux::AgentProgress::format_tool_call(
&fc.name,
&fc.arguments,
))
.await;
let result = if fc.name == "research" {
self.tools
.execute_research(
&fc.arguments,
response_ctx,
room,
&event_id,
0, // depth 0 — orchestrator level
)
.await
} else {
self.tools
.execute(&fc.name, &fc.arguments, response_ctx)
.await
};
let result_str = match result {
Ok(s) => {
let preview: String = s.chars().take(500).collect();
@@ -427,6 +441,9 @@ impl Responder {
debug!(iteration, "Tool iteration complete (conversations)");
}
// Done with tool calls
progress.done().await;
// Extract final text from the last response
if let Some(text) = current_response.assistant_text() {
let text = strip_sol_prefix(&text);