proper Matrix threading + concise tool call formatting
agent_ux: uses Relation::Thread (not Reply) for tool call details. format_tool_call extracts key params instead of dumping raw JSON. format_tool_result truncated to 200 chars. matrix_utils: added make_thread_reply() for threaded responses. sync.rs routes ThreadReply engagement to threaded messages.
This commit is contained in:
104
src/agent_ux.rs
104
src/agent_ux.rs
@@ -1,5 +1,5 @@
|
|||||||
use matrix_sdk::room::Room;
|
use matrix_sdk::room::Room;
|
||||||
use ruma::events::relation::InReplyTo;
|
use ruma::events::relation::Thread;
|
||||||
use ruma::events::room::message::{Relation, RoomMessageEventContent};
|
use ruma::events::room::message::{Relation, RoomMessageEventContent};
|
||||||
use ruma::OwnedEventId;
|
use ruma::OwnedEventId;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
@@ -47,25 +47,19 @@ impl AgentProgress {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Post a step update to the thread. Creates the thread on first call.
|
/// Post a step update to the thread on the user's message.
|
||||||
pub async fn post_step(&mut self, text: &str) {
|
pub async fn post_step(&mut self, text: &str) {
|
||||||
let content = if let Some(ref _root) = self.thread_root_id {
|
let latest = self
|
||||||
// Reply in existing thread
|
.thread_root_id
|
||||||
let mut msg = RoomMessageEventContent::text_markdown(text);
|
.as_ref()
|
||||||
msg.relates_to = Some(Relation::Reply {
|
.unwrap_or(&self.user_event_id)
|
||||||
in_reply_to: InReplyTo::new(self.user_event_id.clone()),
|
.clone();
|
||||||
});
|
|
||||||
msg
|
|
||||||
} else {
|
|
||||||
// First message — starts the thread as a reply to the user's message
|
|
||||||
let mut msg = RoomMessageEventContent::text_markdown(text);
|
|
||||||
msg.relates_to = Some(Relation::Reply {
|
|
||||||
in_reply_to: InReplyTo::new(self.user_event_id.clone()),
|
|
||||||
});
|
|
||||||
msg
|
|
||||||
};
|
|
||||||
|
|
||||||
match self.room.send(content).await {
|
let mut msg = RoomMessageEventContent::text_markdown(text);
|
||||||
|
let thread = Thread::plain(self.user_event_id.clone(), latest);
|
||||||
|
msg.relates_to = Some(Relation::Thread(thread));
|
||||||
|
|
||||||
|
match self.room.send(msg).await {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
if self.thread_root_id.is_none() {
|
if self.thread_root_id.is_none() {
|
||||||
self.thread_root_id = Some(response.event_id);
|
self.thread_root_id = Some(response.event_id);
|
||||||
@@ -96,19 +90,51 @@ impl AgentProgress {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format a tool call for the thread.
|
/// Format a tool call for the thread — concise, not raw args.
|
||||||
pub fn format_tool_call(name: &str, args: &str) -> String {
|
pub fn format_tool_call(name: &str, args: &str) -> String {
|
||||||
format!("`{name}` → ```json\n{args}\n```")
|
// Extract just the key params, not the full JSON blob
|
||||||
|
let summary = match serde_json::from_str::<serde_json::Value>(args) {
|
||||||
|
Ok(v) => {
|
||||||
|
let params: Vec<String> = v
|
||||||
|
.as_object()
|
||||||
|
.map(|obj| {
|
||||||
|
obj.iter()
|
||||||
|
.filter(|(_, v)| !v.is_null() && v.as_str() != Some(""))
|
||||||
|
.map(|(k, v)| {
|
||||||
|
let val = match v {
|
||||||
|
serde_json::Value::String(s) => {
|
||||||
|
if s.len() > 40 {
|
||||||
|
format!("{}…", &s[..40])
|
||||||
|
} else {
|
||||||
|
s.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => other.to_string(),
|
||||||
|
};
|
||||||
|
format!("{k}={val}")
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
if params.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(" ({})", params.join(", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => String::new(),
|
||||||
|
};
|
||||||
|
format!("🔧 `{name}`{summary}")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format a tool result for the thread.
|
/// Format a tool result for the thread — short summary only.
|
||||||
pub fn format_tool_result(name: &str, result: &str) -> String {
|
pub fn format_tool_result(name: &str, result: &str) -> String {
|
||||||
let truncated = if result.len() > 500 {
|
let truncated = if result.len() > 200 {
|
||||||
format!("{}…", &result[..500])
|
format!("{}…", &result[..200])
|
||||||
} else {
|
} else {
|
||||||
result.to_string()
|
result.to_string()
|
||||||
};
|
};
|
||||||
format!("`{name}` ← {truncated}")
|
format!("← `{name}`: {truncated}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,23 +143,41 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_tool_call() {
|
fn test_format_tool_call_with_params() {
|
||||||
let formatted = AgentProgress::format_tool_call("search_archive", r#"{"query":"test"}"#);
|
let formatted = AgentProgress::format_tool_call("search_archive", r#"{"query":"test","room":"general"}"#);
|
||||||
assert!(formatted.contains("search_archive"));
|
assert!(formatted.contains("search_archive"));
|
||||||
assert!(formatted.contains("test"));
|
assert!(formatted.contains("query=test"));
|
||||||
|
assert!(formatted.contains("room=general"));
|
||||||
|
assert!(formatted.starts_with("🔧"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_tool_call_no_params() {
|
||||||
|
let formatted = AgentProgress::format_tool_call("list_rooms", "{}");
|
||||||
|
assert_eq!(formatted, "🔧 `list_rooms`");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_tool_call_truncates_long_values() {
|
||||||
|
let long_code = "x".repeat(100);
|
||||||
|
let args = format!(r#"{{"code":"{}"}}"#, long_code);
|
||||||
|
let formatted = AgentProgress::format_tool_call("run_script", &args);
|
||||||
|
assert!(formatted.contains("code="));
|
||||||
|
assert!(formatted.contains("…"));
|
||||||
|
assert!(formatted.len() < 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_tool_result_truncation() {
|
fn test_format_tool_result_truncation() {
|
||||||
let long = "x".repeat(1000);
|
let long = "x".repeat(500);
|
||||||
let formatted = AgentProgress::format_tool_result("search", &long);
|
let formatted = AgentProgress::format_tool_result("search", &long);
|
||||||
assert!(formatted.len() < 600);
|
assert!(formatted.len() < 300);
|
||||||
assert!(formatted.ends_with('…'));
|
assert!(formatted.ends_with('…'));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_tool_result_short() {
|
fn test_format_tool_result_short() {
|
||||||
let formatted = AgentProgress::format_tool_result("search", "3 results found");
|
let formatted = AgentProgress::format_tool_result("search", "3 results found");
|
||||||
assert_eq!(formatted, "`search` ← 3 results found");
|
assert_eq!(formatted, "← `search`: 3 results found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,19 @@ pub fn make_reply_content(body: &str, reply_to_event_id: OwnedEventId) -> RoomMe
|
|||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a threaded reply — shows up in Matrix threads UI.
|
||||||
|
/// The thread root is the event being replied to (creates the thread on first use).
|
||||||
|
pub fn make_thread_reply(
|
||||||
|
body: &str,
|
||||||
|
thread_root_id: OwnedEventId,
|
||||||
|
) -> RoomMessageEventContent {
|
||||||
|
use ruma::events::relation::Thread;
|
||||||
|
let mut content = RoomMessageEventContent::text_markdown(body);
|
||||||
|
let thread = Thread::plain(thread_root_id.clone(), thread_root_id);
|
||||||
|
content.relates_to = Some(Relation::Thread(thread));
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
/// Send an emoji reaction to a message.
|
/// Send an emoji reaction to a message.
|
||||||
pub async fn send_reaction(
|
pub async fn send_reaction(
|
||||||
room: &Room,
|
room: &Room,
|
||||||
|
|||||||
Reference in New Issue
Block a user