refactor: remove threads layer, use sessions directly for ACP (#9078)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Douwe Osinga
2026-05-08 16:10:55 -04:00
committed by GitHub
parent a999f52056
commit ea5802c380
32 changed files with 525 additions and 1922 deletions
@@ -108,6 +108,9 @@ pub async fn run_list_sessions<C: Connection>() {
if let Some(ref mut meta) = s.meta {
assert!(meta.get("createdAt").and_then(|v| v.as_str()).is_some());
meta.remove("createdAt");
// Provider/model metadata varies by test fixture; not relevant here.
meta.remove("providerId");
meta.remove("modelId");
}
}
let mut expected_meta = serde_json::Map::new();
@@ -1,197 +0,0 @@
use goose::conversation::message::Message;
use goose::session::session_manager::SessionStorage;
use goose::session::thread_manager::ThreadManager;
use rmcp::model::CallToolRequestParams;
use std::sync::Arc;
use tempfile::TempDir;
async fn setup() -> (ThreadManager, TempDir) {
let tmp = TempDir::new().unwrap();
let storage = SessionStorage::create(tmp.path()).await.unwrap();
let tm = ThreadManager::new(Arc::new(storage));
(tm, tmp)
}
#[tokio::test]
async fn consecutive_text_chunks_are_coalesced() {
let (tm, _tmp) = setup().await;
let thread = tm.create_thread(None, None, None).await.unwrap();
// Simulate streaming: three consecutive assistant text chunks.
tm.append_message(
&thread.id,
Some("s1"),
&Message::assistant().with_text("Hello"),
)
.await
.unwrap();
tm.append_message(
&thread.id,
Some("s1"),
&Message::assistant().with_text(" world"),
)
.await
.unwrap();
tm.append_message(&thread.id, Some("s1"), &Message::assistant().with_text("!"))
.await
.unwrap();
let messages = tm.list_messages(&thread.id).await.unwrap();
assert_eq!(messages.len(), 1, "should coalesce into a single row");
assert_eq!(messages[0].as_concat_text(), "Hello world!");
}
#[tokio::test]
async fn role_change_prevents_coalescing() {
let (tm, _tmp) = setup().await;
let thread = tm.create_thread(None, None, None).await.unwrap();
tm.append_message(&thread.id, Some("s1"), &Message::user().with_text("Hi"))
.await
.unwrap();
tm.append_message(
&thread.id,
Some("s1"),
&Message::assistant().with_text("Hey"),
)
.await
.unwrap();
let messages = tm.list_messages(&thread.id).await.unwrap();
assert_eq!(messages.len(), 2, "different roles should not coalesce");
assert_eq!(messages[0].as_concat_text(), "Hi");
assert_eq!(messages[1].as_concat_text(), "Hey");
}
#[tokio::test]
async fn non_text_content_breaks_coalescing() {
let (tm, _tmp) = setup().await;
let thread = tm.create_thread(None, None, None).await.unwrap();
// Text, then tool request, then more text — should be 3 rows.
tm.append_message(
&thread.id,
Some("s1"),
&Message::assistant().with_text("Let me check"),
)
.await
.unwrap();
let tool_msg = Message::assistant().with_tool_request(
"call_1",
Ok(CallToolRequestParams::new("shell").with_arguments(
serde_json::json!({"command": "ls"})
.as_object()
.unwrap()
.clone(),
)),
);
tm.append_message(&thread.id, Some("s1"), &tool_msg)
.await
.unwrap();
tm.append_message(
&thread.id,
Some("s1"),
&Message::assistant().with_text("Done"),
)
.await
.unwrap();
let messages = tm.list_messages(&thread.id).await.unwrap();
assert_eq!(messages.len(), 3, "tool request should break coalescing");
assert_eq!(messages[0].as_concat_text(), "Let me check");
assert_eq!(messages[2].as_concat_text(), "Done");
}
#[tokio::test]
async fn text_after_tool_response_not_coalesced_with_tool() {
let (tm, _tmp) = setup().await;
let thread = tm.create_thread(None, None, None).await.unwrap();
// A tool request message (non-text) followed by text — should not coalesce.
let tool_msg = Message::assistant().with_tool_request(
"call_1",
Ok(CallToolRequestParams::new("shell").with_arguments(
serde_json::json!({"command": "ls"})
.as_object()
.unwrap()
.clone(),
)),
);
tm.append_message(&thread.id, Some("s1"), &tool_msg)
.await
.unwrap();
tm.append_message(
&thread.id,
Some("s1"),
&Message::assistant().with_text("Result"),
)
.await
.unwrap();
let messages = tm.list_messages(&thread.id).await.unwrap();
assert_eq!(
messages.len(),
2,
"text should not coalesce with non-text predecessor"
);
}
#[tokio::test]
async fn empty_message_not_coalesced() {
let (tm, _tmp) = setup().await;
let thread = tm.create_thread(None, None, None).await.unwrap();
tm.append_message(
&thread.id,
Some("s1"),
&Message::assistant().with_text("Hello"),
)
.await
.unwrap();
// An empty assistant message (no content items).
let empty = Message::assistant();
tm.append_message(&thread.id, Some("s1"), &empty)
.await
.unwrap();
let messages = tm.list_messages(&thread.id).await.unwrap();
// Empty message should be inserted as a new row (not coalesced).
assert_eq!(messages.len(), 2);
assert_eq!(messages[0].as_concat_text(), "Hello");
}
#[tokio::test]
async fn metadata_change_prevents_coalescing() {
let (tm, _tmp) = setup().await;
let thread = tm.create_thread(None, None, None).await.unwrap();
tm.append_message(
&thread.id,
Some("s1"),
&Message::assistant().with_text("Visible"),
)
.await
.unwrap();
tm.append_message(
&thread.id,
Some("s1"),
&Message::assistant().with_text(" hidden").agent_only(),
)
.await
.unwrap();
let messages = tm.list_messages(&thread.id).await.unwrap();
assert_eq!(
messages.len(),
2,
"metadata boundary should break coalescing"
);
assert!(messages[0].metadata.user_visible);
assert!(!messages[1].metadata.user_visible);
assert_eq!(messages[0].as_concat_text(), "Visible");
assert_eq!(messages[1].as_concat_text(), " hidden");
}