Skip to content

Commit cb61817

Browse files
committed
feat(search): default session search to message events
1 parent ea38460 commit cb61817

8 files changed

Lines changed: 264 additions & 36 deletions

File tree

crates/moraine-conversations/tests/repository_integration/search.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,7 @@ async fn search_mcp_events_supports_global_search_with_enriched_hits() {
594594
n_hits: Some(10),
595595
event_types: Some(vec![
596596
McpEventType::ToolResponse,
597+
McpEventType::ToolCall,
597598
McpEventType::UserInput,
598599
McpEventType::AssistantResponse,
599600
]),
@@ -611,6 +612,7 @@ async fn search_mcp_events_supports_global_search_with_enriched_hits() {
611612
vec![
612613
McpEventType::UserInput,
613614
McpEventType::AssistantResponse,
615+
McpEventType::ToolCall,
614616
McpEventType::ToolResponse
615617
]
616618
);
@@ -872,6 +874,63 @@ async fn search_mcp_events_supports_turn_scoped_search() {
872874
assert!(search_query.contains("e.session_id = 'sess_c' AND e.turn_seq = 2"));
873875
assert!(search_query.contains("ALL INNER JOIN `moraine`.`mcp_open_turns` AS scope_t FINAL"));
874876
}
877+
#[tokio::test(flavor = "multi_thread")]
878+
async fn search_mcp_events_returns_explicit_tool_event_filters() {
879+
let (repo, _state) = build_repo().await;
880+
881+
let tool_call_result = repo
882+
.search_mcp_events(SearchMcpEventsQuery {
883+
query: "hello world".to_string(),
884+
n_hits: Some(5),
885+
event_types: Some(vec![McpEventType::ToolCall]),
886+
min_score: Some(0.0),
887+
min_should_match: Some(1),
888+
..SearchMcpEventsQuery::default()
889+
})
890+
.await
891+
.expect("tool-call search");
892+
let tool_response_result = repo
893+
.search_mcp_events(SearchMcpEventsQuery {
894+
query: "hello world".to_string(),
895+
n_hits: Some(5),
896+
event_types: Some(vec![McpEventType::ToolResponse]),
897+
min_score: Some(0.0),
898+
min_should_match: Some(1),
899+
..SearchMcpEventsQuery::default()
900+
})
901+
.await
902+
.expect("tool-response search");
903+
let mixed_result = repo
904+
.search_mcp_events(SearchMcpEventsQuery {
905+
query: "hello world".to_string(),
906+
n_hits: Some(5),
907+
event_types: Some(vec![McpEventType::UserInput, McpEventType::ToolCall]),
908+
min_score: Some(0.0),
909+
min_should_match: Some(1),
910+
..SearchMcpEventsQuery::default()
911+
})
912+
.await
913+
.expect("mixed message and tool search");
914+
915+
assert_eq!(tool_call_result.hits.len(), 1);
916+
assert_eq!(tool_call_result.hits[0].event_type, McpEventType::ToolCall);
917+
assert_eq!(tool_call_result.hits[0].event_uid, "evt-c-tool-call");
918+
assert_eq!(tool_response_result.hits.len(), 1);
919+
assert_eq!(
920+
tool_response_result.hits[0].event_type,
921+
McpEventType::ToolResponse
922+
);
923+
assert_eq!(tool_response_result.hits[0].event_uid, "evt-c-tool");
924+
assert_eq!(
925+
mixed_result
926+
.hits
927+
.iter()
928+
.map(|hit| hit.event_type)
929+
.collect::<Vec<_>>(),
930+
vec![McpEventType::ToolCall, McpEventType::UserInput]
931+
);
932+
}
933+
875934
#[tokio::test(flavor = "multi_thread")]
876935
async fn search_mcp_events_event_type_filter_distinguishes_user_and_assistant_messages() {
877936
let (repo, state) = build_repo().await;
@@ -898,6 +957,20 @@ async fn search_mcp_events_event_type_filter_distinguishes_user_and_assistant_me
898957
})
899958
.await
900959
.expect("assistant response search");
960+
let message_result = repo
961+
.search_mcp_events(SearchMcpEventsQuery {
962+
query: "hello world".to_string(),
963+
n_hits: Some(5),
964+
event_types: Some(vec![
965+
McpEventType::UserInput,
966+
McpEventType::AssistantResponse,
967+
]),
968+
min_score: Some(0.0),
969+
min_should_match: Some(1),
970+
..SearchMcpEventsQuery::default()
971+
})
972+
.await
973+
.expect("message-only search");
901974

902975
assert_eq!(user_result.hits.len(), 1);
903976
assert_eq!(user_result.hits[0].event_uid, "evt-c-user");
@@ -910,6 +983,14 @@ async fn search_mcp_events_event_type_filter_distinguishes_user_and_assistant_me
910983
McpEventType::AssistantResponse
911984
);
912985
assert_eq!(assistant_result.hits[0].actor_role, "assistant");
986+
assert!(message_result.hits.iter().all(|hit| matches!(
987+
hit.event_type,
988+
McpEventType::UserInput | McpEventType::AssistantResponse
989+
)));
990+
assert_eq!(
991+
message_result.event_types,
992+
vec![McpEventType::UserInput, McpEventType::AssistantResponse]
993+
);
913994

914995
let queries = state.queries.lock().expect("queries lock").clone();
915996
assert!(queries.iter().any(|q| {

crates/moraine-conversations/tests/repository_integration/support/mock_clickhouse.rs

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -871,18 +871,48 @@ pub(crate) async fn spawn_mock_server(options: MockOptions) -> (String, Arc<Mock
871871
.and_then(|(_, tail)| tail.split_once("GROUP BY p.doc_id"))
872872
.map(|(filter, _)| filter)
873873
.unwrap_or(query.as_str());
874+
let includes_user = filter_clause.contains("lowerUTF8(p.actor_role) = 'user'");
875+
let includes_assistant =
876+
filter_clause.contains("lowerUTF8(p.actor_role) = 'assistant'");
877+
let includes_tool_call = filter_clause.contains("p.event_class = 'tool_call'");
878+
let includes_tool_response = filter_clause.contains("p.event_class = 'tool_result'");
874879
let mut rows = if query.contains("e.session_id = 'sess_c' AND e.turn_seq = 2") {
875880
vec![candidate("evt-c-tool", 13.0, 2, 1_767_434_430_000)]
876881
} else if query.contains("p.session_id = 'sess_a'") {
877882
vec![candidate("evt-a-11", 7.0, 1, 1_767_261_720_000)]
878-
} else if filter_clause.contains("lowerUTF8(p.actor_role) = 'user'")
879-
&& !filter_clause.contains("lowerUTF8(p.actor_role) = 'assistant'")
883+
} else if includes_user
884+
&& !includes_assistant
885+
&& !includes_tool_call
886+
&& !includes_tool_response
880887
{
881888
vec![candidate("evt-c-user", 11.0, 2, 1_767_434_460_000)]
882-
} else if filter_clause.contains("lowerUTF8(p.actor_role) = 'assistant'")
883-
&& !filter_clause.contains("lowerUTF8(p.actor_role) = 'user'")
889+
} else if includes_assistant
890+
&& !includes_user
891+
&& !includes_tool_call
892+
&& !includes_tool_response
884893
{
885894
vec![candidate("evt-c-42", 12.5, 2, 1_767_434_520_000)]
895+
} else if includes_tool_call
896+
&& !includes_user
897+
&& !includes_assistant
898+
&& !includes_tool_response
899+
{
900+
vec![candidate("evt-c-tool-call", 13.5, 2, 1_767_434_400_000)]
901+
} else if includes_tool_response
902+
&& !includes_user
903+
&& !includes_assistant
904+
&& !includes_tool_call
905+
{
906+
vec![candidate("evt-c-tool", 13.0, 2, 1_767_434_430_000)]
907+
} else if includes_user
908+
&& includes_tool_call
909+
&& !includes_assistant
910+
&& !includes_tool_response
911+
{
912+
vec![
913+
candidate("evt-c-tool-call", 13.5, 2, 1_767_434_400_000),
914+
candidate("evt-c-user", 11.0, 2, 1_767_434_460_000),
915+
]
886916
} else if query.contains("LIMIT 3 OFFSET 3")
887917
&& !state.options.repeat_duplicate_search_pages
888918
{
@@ -925,6 +955,13 @@ pub(crate) async fn spawn_mock_server(options: MockOptions) -> (String, Arc<Mock
925955
9_u64,
926956
1_u32,
927957
),
958+
"evt-c-tool-call" => (
959+
"sess_c",
960+
"2026-01-03 10:00:00",
961+
1_767_434_400_000_i64,
962+
39_u64,
963+
2_u32,
964+
),
928965
"evt-c-tool" => (
929966
"sess_c",
930967
"2026-01-03 10:00:30",
@@ -954,25 +991,29 @@ pub(crate) async fn spawn_mock_server(options: MockOptions) -> (String, Arc<Mock
954991
2_u32,
955992
),
956993
};
957-
let is_tool = event_uid == "evt-c-tool";
994+
let is_tool_call = event_uid == "evt-c-tool-call";
995+
let is_tool_response = event_uid == "evt-c-tool";
958996
let is_user = event_uid == "evt-c-user";
959997
let is_duplicate = event_uid == "evt-c-duplicate";
960998
let is_canonical_response = event_uid == "evt-c-42";
961-
let actor_role = if is_tool {
999+
let actor_role = if is_tool_response {
9621000
"tool"
9631001
} else if is_user {
9641002
"user"
9651003
} else {
9661004
"assistant"
9671005
};
968-
let event_type = if is_tool {
1006+
let event_type = if is_tool_call {
1007+
"tool_call"
1008+
} else if is_tool_response {
9691009
"tool_response"
9701010
} else if is_user {
9711011
"user_input"
9721012
} else {
9731013
"assistant_response"
9741014
};
9751015
let text = match event_uid {
1016+
"evt-c-tool-call" => "assistant invoked bash for hello world",
9761017
"evt-c-tool" => "cargo test failure output with stack details",
9771018
"evt-c-user" => "user asked about hello world in a prompt",
9781019
"evt-a-11" => "weaker assistant event in session a with extra context",
@@ -986,11 +1027,11 @@ pub(crate) async fn spawn_mock_server(options: MockOptions) -> (String, Arc<Mock
9861027
"harness": "codex",
9871028
"inference_provider": "openai",
9881029
"endpoint_kind": "generation",
989-
"event_class": if is_tool { "tool_result" } else if is_duplicate { "event_msg" } else { "message" },
990-
"payload_type": if is_tool { "tool_result" } else if is_duplicate { "agent_message" } else if is_canonical_response { "message" } else { "text" },
1030+
"event_class": if is_tool_call { "tool_call" } else if is_tool_response { "tool_result" } else if is_duplicate { "event_msg" } else { "message" },
1031+
"payload_type": if is_tool_call { "tool_use" } else if is_tool_response { "tool_result" } else if is_duplicate { "agent_message" } else if is_canonical_response { "message" } else { "text" },
9911032
"actor_role": actor_role,
992-
"name": if is_tool { "bash" } else { "" },
993-
"phase": if is_tool || is_duplicate { "completed" } else if is_canonical_response { "final_answer" } else { "" },
1033+
"name": if is_tool_call || is_tool_response { "bash" } else { "" },
1034+
"phase": if is_tool_call || is_tool_response || is_duplicate { "completed" } else if is_canonical_response { "final_answer" } else { "" },
9941035
"payload_phase": if is_duplicate || is_canonical_response { "final_answer" } else { "" },
9951036
"source_ref": format!("/tmp/{session_id}.jsonl:1:{event_order}"),
9961037
"doc_len": 19_u32,
@@ -1005,11 +1046,11 @@ pub(crate) async fn spawn_mock_server(options: MockOptions) -> (String, Arc<Mock
10051046
"event_unix_ms": event_unix_ms,
10061047
"event_order": event_order,
10071048
"turn_seq": turn_seq,
1008-
"event_ordinal": if is_tool { 1_u32 } else if is_user { 2_u32 } else if session_id == "sess_c" { 3_u32 } else { 1_u32 },
1049+
"event_ordinal": if is_tool_call || is_tool_response { 1_u32 } else if is_user { 2_u32 } else if session_id == "sess_c" { 3_u32 } else { 1_u32 },
10091050
"turn_event_count": if session_id == "sess_c" { 3_u64 } else { 1_u64 },
10101051
"turn_completed": if session_id == "sess_c" { 1_u8 } else { 0_u8 },
10111052
"turn_terminal_event_uid": if session_id == "sess_c" { "evt-c-42" } else { "" },
1012-
"call_id": if is_tool { "call-bash-1" } else { "" },
1053+
"call_id": if is_tool_call || is_tool_response { "call-bash-1" } else { "" },
10131054
"item_id": format!("item-{event_uid}"),
10141055
"model": "gpt-5.3-codex",
10151056
"session_started_at_unix_ms": event_unix_ms - 120_000,
@@ -1021,6 +1062,7 @@ pub(crate) async fn spawn_mock_server(options: MockOptions) -> (String, Arc<Mock
10211062
})
10221063
};
10231064
let event_uids = [
1065+
"evt-c-tool-call",
10241066
"evt-c-tool",
10251067
"evt-c-user",
10261068
"evt-c-42",

crates/moraine-mcp-core/src/contract.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ impl McpEventType {
412412
}
413413

414414
pub fn search_defaults() -> &'static [McpEventType] {
415-
&[Self::UserInput, Self::AssistantResponse, Self::ToolResponse]
415+
&[Self::UserInput, Self::AssistantResponse]
416416
}
417417

418418
pub fn is_searchable(self) -> bool {
@@ -1659,14 +1659,10 @@ mod tests {
16591659
}
16601660

16611661
#[test]
1662-
fn default_event_types_are_user_assistant_and_tool_response() {
1662+
fn default_event_types_are_user_and_assistant_messages() {
16631663
assert_eq!(
16641664
default_search_event_types(),
1665-
vec![
1666-
McpEventType::UserInput,
1667-
McpEventType::AssistantResponse,
1668-
McpEventType::ToolResponse
1669-
]
1665+
vec![McpEventType::UserInput, McpEventType::AssistantResponse]
16701666
);
16711667

16721668
let canonical = SearchSessionsArgs {
@@ -1919,7 +1915,7 @@ mod tests {
19191915
json!({
19201916
"query": "migration",
19211917
"within_id": null,
1922-
"event_types": ["user_input", "assistant_response", "tool_response"],
1918+
"event_types": ["user_input", "assistant_response"],
19231919
"n_hits": 10
19241920
}),
19251921
json!({
@@ -1939,7 +1935,7 @@ mod tests {
19391935
"request": {
19401936
"query": "migration",
19411937
"within_id": null,
1942-
"event_types": ["user_input", "assistant_response", "tool_response"],
1938+
"event_types": ["user_input", "assistant_response"],
19431939
"n_hits": 10
19441940
},
19451941
"data": {

crates/moraine-mcp-core/src/lib.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ impl AppState {
427427
"tools": [
428428
{
429429
"name": contract::SEARCH_SESSIONS_TOOL,
430-
"description": "Search Moraine session history and return compact event-ranked handles. Use open with the returned event_id, turn_id, or session_id to expand results.",
430+
"description": "Search Moraine session history and return compact event-ranked handles. By default, searches user_input and assistant_response events. Select tool_call or tool_response with event_types for raw tool evidence, then use open on a returned turn_id or session_id for full context.",
431431
"inputSchema": {
432432
"type": "object",
433433
"additionalProperties": false,
@@ -455,7 +455,8 @@ impl AppState {
455455
"runtime"
456456
]
457457
},
458-
"description": "Optional normalized event type filter. Defaults to user_input, assistant_response, and tool_response."
458+
"default": ["user_input", "assistant_response"],
459+
"description": "Optional normalized event type filter. Defaults to user_input and assistant_response. Select tool_call or tool_response explicitly for raw tool evidence."
459460
},
460461
"harness": {
461462
"type": ["string", "null"],
@@ -2287,13 +2288,21 @@ mod tests {
22872288
.iter()
22882289
.find(|tool| tool["name"].as_str() == Some("search_sessions"))
22892290
.expect("search_sessions exists");
2291+
assert_eq!(
2292+
search["description"],
2293+
json!("Search Moraine session history and return compact event-ranked handles. By default, searches user_input and assistant_response events. Select tool_call or tool_response with event_types for raw tool evidence, then use open on a returned turn_id or session_id for full context.")
2294+
);
22902295
assert_eq!(
22912296
search["inputSchema"]["properties"]["query"]["description"],
22922297
json!("Keyword (BM25) search query. Matching is bag-of-words: quotes and punctuation are ignored, and a quoted phrase is matched as independent terms subject to the configured minimum-match threshold, not as an exact phrase.")
22932298
);
22942299
assert_eq!(
22952300
search["inputSchema"]["properties"]["event_types"]["description"],
2296-
json!("Optional normalized event type filter. Defaults to user_input, assistant_response, and tool_response.")
2301+
json!("Optional normalized event type filter. Defaults to user_input and assistant_response. Select tool_call or tool_response explicitly for raw tool evidence.")
2302+
);
2303+
assert_eq!(
2304+
search["inputSchema"]["properties"]["event_types"]["default"],
2305+
json!(["user_input", "assistant_response"])
22972306
);
22982307
assert_eq!(
22992308
search["inputSchema"]["properties"]["n_hits"]["default"],

0 commit comments

Comments
 (0)