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 ruma::events::relation::InReplyTo;
|
||||
use ruma::events::relation::Thread;
|
||||
use ruma::events::room::message::{Relation, RoomMessageEventContent};
|
||||
use ruma::OwnedEventId;
|
||||
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) {
|
||||
let content = if let Some(ref _root) = self.thread_root_id {
|
||||
// Reply in existing thread
|
||||
let mut msg = RoomMessageEventContent::text_markdown(text);
|
||||
msg.relates_to = Some(Relation::Reply {
|
||||
in_reply_to: InReplyTo::new(self.user_event_id.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
|
||||
};
|
||||
let latest = self
|
||||
.thread_root_id
|
||||
.as_ref()
|
||||
.unwrap_or(&self.user_event_id)
|
||||
.clone();
|
||||
|
||||
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) => {
|
||||
if self.thread_root_id.is_none() {
|
||||
self.thread_root_id = Some(response.event_id);
|
||||
@@ -96,19 +90,51 @@ impl AgentProgress {
|
||||
.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 {
|
||||
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 {
|
||||
let truncated = if result.len() > 500 {
|
||||
format!("{}…", &result[..500])
|
||||
let truncated = if result.len() > 200 {
|
||||
format!("{}…", &result[..200])
|
||||
} else {
|
||||
result.to_string()
|
||||
};
|
||||
format!("`{name}` ← {truncated}")
|
||||
format!("← `{name}`: {truncated}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,23 +143,41 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_tool_call() {
|
||||
let formatted = AgentProgress::format_tool_call("search_archive", r#"{"query":"test"}"#);
|
||||
fn test_format_tool_call_with_params() {
|
||||
let formatted = AgentProgress::format_tool_call("search_archive", r#"{"query":"test","room":"general"}"#);
|
||||
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]
|
||||
fn test_format_tool_result_truncation() {
|
||||
let long = "x".repeat(1000);
|
||||
let long = "x".repeat(500);
|
||||
let formatted = AgentProgress::format_tool_result("search", &long);
|
||||
assert!(formatted.len() < 600);
|
||||
assert!(formatted.len() < 300);
|
||||
assert!(formatted.ends_with('…'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_tool_result_short() {
|
||||
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
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub async fn send_reaction(
|
||||
room: &Room,
|
||||
|
||||
Reference in New Issue
Block a user