use std::sync::Arc;
use async_trait::async_trait;
use smooth_operator_core::tool::ToolSchema;
use smooth_operator_core::Tool;
use crate::adapter::{MessageQuery, StorageAdapter};
use crate::domain::Direction;
const DEFAULT_LIMIT: usize = 20;
const MAX_LIMIT: usize = 100;
pub struct ConversationHistoryTool {
storage: Arc<dyn StorageAdapter>,
conversation_id: String,
}
impl ConversationHistoryTool {
#[must_use]
pub fn new(storage: Arc<dyn StorageAdapter>, conversation_id: impl Into<String>) -> Self {
Self {
storage,
conversation_id: conversation_id.into(),
}
}
}
#[async_trait]
impl Tool for ConversationHistoryTool {
fn schema(&self) -> ToolSchema {
ToolSchema {
name: "conversation_history".to_string(),
description: "Read the most recent messages of the CURRENT conversation (oldest-first \
within the returned window). Use this to recall earlier details the user \
mentioned, or to summarize the discussion so far. Returns each message's \
direction (user vs agent) and text."
.to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"limit": {
"type": "integer",
"description": "Maximum number of recent messages to return (default 20).",
"minimum": 1,
"maximum": 100
}
},
"required": []
}),
}
}
async fn execute(&self, arguments: serde_json::Value) -> anyhow::Result<String> {
let limit = arguments
.get("limit")
.and_then(serde_json::Value::as_u64)
.map_or(DEFAULT_LIMIT, |n| (n as usize).clamp(1, MAX_LIMIT));
let query = MessageQuery {
conversation_id: self.conversation_id.clone(),
limit,
cursor: None,
descending: true,
};
let mut page = self.storage.list_messages_by_conversation(query).await?;
page.messages.reverse();
if page.messages.is_empty() {
return Ok("No messages in this conversation yet.".to_string());
}
let mut out = format!(
"Recent conversation history ({} message(s), oldest-first):\n",
page.messages.len()
);
for m in &page.messages {
let speaker = match m.direction {
Direction::Inbound => "User",
Direction::Outbound => "Agent",
};
let text = m
.content
.text
.clone()
.or_else(|| m.content.items.iter().find_map(|it| it.text.clone()))
.unwrap_or_default();
out.push_str(&format!("- {speaker}: {text}\n"));
}
Ok(out)
}
fn is_read_only(&self) -> bool {
true
}
}