Add occurrence search for paginated threads (#33907)

## What changed

- Add the experimental `thread/searchOccurrences` app-server method for case-insensitive literal search across visible user messages and final assistant messages without replaying the thread.
- Return paginated snippets with UTF-16 match ranges and `thread/turns/list` cursors for navigating to each matching turn.
- Index persisted item types to efficiently select searchable messages from paginated thread history.

## Testing

- Add an app-server integration test covering pagination, steering messages, final-answer selection, Markdown normalization, and UTF-16 offsets.

GitOrigin-RevId: e80160e0f83eb4e5db8de862307085093bee362b
This commit is contained in:
Owen Lin
2026-07-17 21:24:26 +00:00
committed by copyberry
parent 9f6c29e281
commit c1862b8db4
21 changed files with 875 additions and 23 deletions
@@ -630,6 +630,13 @@ client_request_definitions! {
serialization: None,
response: v2::ThreadSearchResponse,
},
#[experimental("thread/searchOccurrences")]
ThreadSearchOccurrences => "thread/searchOccurrences" {
params: v2::ThreadSearchOccurrencesParams,
// Explicitly concurrent: this reads persisted paginated history.
serialization: None,
response: v2::ThreadSearchOccurrencesResponse,
},
ThreadLoadedList => "thread/loaded/list" {
params: v2::ThreadLoadedListParams,
serialization: None,
@@ -1249,6 +1249,58 @@ pub struct ThreadSearchResponse {
pub backwards_cursor: Option<String>,
}
/// Parameters for searching visible message occurrences within one paginated thread.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadSearchOccurrencesParams {
pub thread_id: String,
/// Case-insensitive literal substring to find in visible user messages and final assistant
/// messages.
pub search_term: String,
/// Opaque cursor returned by a previous call for the same thread and search term.
#[ts(optional = nullable)]
pub cursor: Option<String>,
/// Optional occurrence page size.
#[ts(optional = nullable)]
pub limit: Option<u32>,
}
/// UTF-16 code-unit range within `snippet`.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadSearchTextRange {
/// Inclusive UTF-16 code-unit offset.
pub start: u32,
/// Exclusive UTF-16 code-unit offset.
pub end: u32,
}
/// One visible message occurrence returned by [`ThreadSearchOccurrencesResponse`].
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadSearchOccurrence {
pub turn_id: String,
pub item_id: String,
pub snippet: String,
/// Match range within `snippet`, in UTF-16 code units.
pub snippet_match_range: ThreadSearchTextRange,
/// Opaque inclusive cursor accepted by `thread/turns/list` for this turn.
pub turn_cursor: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadSearchOccurrencesResponse {
/// Occurrences in chronological message order.
pub data: Vec<ThreadSearchOccurrence>,
/// Opaque cursor to continue after the last returned occurrence.
pub next_cursor: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]