feat(app-server, threadstore): Thread pagination APIs and ThreadStore contract (#21566)

## Why
The goal of this PR is to align on app-server and `ThreadStore` API
updates for paginating through large threads.


#### app-server
##### `thread/turns/list`
- Updates `thread/turns/list` to support `itemsView?: "notLoaded" |
"summary" | "full" | null`, defaulting to `summary`.
- Implements the current `thread/turns/list` behavior over the existing
persisted rollout-history fallback:
  - `notLoaded` returns turn envelopes with empty `items`.
- `summary` returns the first user message and final assistant message
when available.
  - `full` preserves the existing full item behavior.

Note that this method still uses the naive approach of loading the
entire rollout file, and returns just the filtered slice of the data.
Real pagination will come later by leveraging SQLite.

##### `thread/turns/items/list`
- Adds the experimental `thread/turns/items/list` protocol, schema,
dispatcher, and processor stub. The app-server currently returns
JSON-RPC `-32601` with `thread/turns/items/list is not supported yet`.

#### ThreadStore
- Adds the experimental `thread/turns/items/list` protocol, schema,
dispatcher, and processor stub. The app-server currently returns
JSON-RPC `-32601` with `thread/turns/items/list is not supported yet`.
- Adds `ThreadStore` contract types and stubbed methods for listing
thread turns and listing items within a turn.
- Adds a typed `StoredTurnStatus` and `StoredTurnError` to avoid baking
app-server API enums or lossy string status values into the store-facing
turn contract.
- Adds a typed `StoredTurnStatus` and `StoredTurnError` to avoid baking
app-server API enums or lossy string status values into the store-facing
turn contract.

This also sketches the storage abstraction we expect to need once turns
are indexed/stored. In particular, `notLoaded` is useful only if
ThreadStore can eventually list turn metadata without loading every
persisted item for each turn.

## Validation

- Added/updated protocol serialization coverage for the new request and
response shapes.
- Added app-server integration coverage for `thread/turns/list` default
summary behavior and all three `itemsView` modes.
- Added app-server integration coverage that `thread/turns/items/list`
returns the expected unsupported JSON-RPC error when experimental APIs
are enabled.
- Added thread-store coverage that the default trait methods return
`ThreadStoreError::Unsupported`.

No developers.openai.com documentation update is needed for this
internal experimental app-server API surface.
This commit is contained in:
Owen Lin
2026-05-07 15:44:43 -07:00
committed by GitHub
parent 54ef99a365
commit 0d0835dd53
17 changed files with 608 additions and 5 deletions
+7
View File
@@ -27,6 +27,13 @@ pub enum ThreadStoreError {
message: String,
},
/// The store implementation does not support this operation yet.
#[error("thread-store unsupported operation: {operation}")]
Unsupported {
/// Stable operation name for callers that need to map unsupported operations.
operation: &'static str,
},
/// Catch-all for implementation failures that do not fit a more specific category.
#[error("thread-store internal error: {message}")]
Internal {
+51
View File
@@ -35,6 +35,57 @@ fn stores() -> &'static Mutex<HashMap<String, Arc<InMemoryThreadStore>>> {
IN_MEMORY_THREAD_STORES.get_or_init(|| Mutex::new(HashMap::new()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ListItemsParams;
use crate::ListTurnsParams;
use crate::SortDirection;
use crate::StoredTurnItemsView;
#[tokio::test]
async fn default_turn_pagination_methods_return_unsupported() {
let store = InMemoryThreadStore::default();
let thread_id = ThreadId::default();
let turns_err = store
.list_turns(ListTurnsParams {
thread_id,
include_archived: true,
cursor: None,
page_size: 10,
sort_direction: SortDirection::Asc,
items_view: StoredTurnItemsView::Summary,
})
.await
.expect_err("default list_turns should be unsupported");
assert!(matches!(
turns_err,
ThreadStoreError::Unsupported {
operation: "list_turns"
}
));
let items_err = store
.list_items(ListItemsParams {
thread_id,
turn_id: "turn_1".to_string(),
include_archived: true,
cursor: None,
page_size: 10,
sort_direction: SortDirection::Asc,
})
.await
.expect_err("default list_items should be unsupported");
assert!(matches!(
items_err,
ThreadStoreError::Unsupported {
operation: "list_items"
}
));
}
}
fn stores_guard() -> MutexGuard<'static, HashMap<String, Arc<InMemoryThreadStore>>> {
match stores().lock() {
Ok(guard) => guard,
+8
View File
@@ -26,7 +26,10 @@ pub use types::AppendThreadItemsParams;
pub use types::ArchiveThreadParams;
pub use types::CreateThreadParams;
pub use types::GitInfoPatch;
pub use types::ItemPage;
pub use types::ListItemsParams;
pub use types::ListThreadsParams;
pub use types::ListTurnsParams;
pub use types::LoadThreadHistoryParams;
pub use types::OptionalStringPatch;
pub use types::ReadThreadByRolloutPathParams;
@@ -35,9 +38,14 @@ pub use types::ResumeThreadParams;
pub use types::SortDirection;
pub use types::StoredThread;
pub use types::StoredThreadHistory;
pub use types::StoredTurn;
pub use types::StoredTurnError;
pub use types::StoredTurnItemsView;
pub use types::StoredTurnStatus;
pub use types::ThreadEventPersistenceMode;
pub use types::ThreadMetadataPatch;
pub use types::ThreadPage;
pub use types::ThreadPersistenceMetadata;
pub use types::ThreadSortKey;
pub use types::TurnPage;
pub use types::UpdateThreadMetadataParams;
+19
View File
@@ -5,7 +5,10 @@ use std::any::Any;
use crate::AppendThreadItemsParams;
use crate::ArchiveThreadParams;
use crate::CreateThreadParams;
use crate::ItemPage;
use crate::ListItemsParams;
use crate::ListThreadsParams;
use crate::ListTurnsParams;
use crate::LoadThreadHistoryParams;
use crate::ReadThreadByRolloutPathParams;
use crate::ReadThreadParams;
@@ -13,7 +16,9 @@ use crate::ResumeThreadParams;
use crate::StoredThread;
use crate::StoredThreadHistory;
use crate::ThreadPage;
use crate::ThreadStoreError;
use crate::ThreadStoreResult;
use crate::TurnPage;
use crate::UpdateThreadMetadataParams;
/// Storage-neutral thread persistence boundary.
@@ -67,6 +72,20 @@ pub trait ThreadStore: Any + Send + Sync {
/// Lists stored threads matching the supplied filters.
async fn list_threads(&self, params: ListThreadsParams) -> ThreadStoreResult<ThreadPage>;
/// Lists turns within a stored thread.
async fn list_turns(&self, _params: ListTurnsParams) -> ThreadStoreResult<TurnPage> {
Err(ThreadStoreError::Unsupported {
operation: "list_turns",
})
}
/// Lists persisted items within a stored turn.
async fn list_items(&self, _params: ListItemsParams) -> ThreadStoreResult<ItemPage> {
Err(ThreadStoreError::Unsupported {
operation: "list_items",
})
}
/// Applies a mutable metadata patch and returns the updated thread.
async fn update_thread_metadata(
&self,
+111
View File
@@ -183,6 +183,117 @@ pub struct ThreadPage {
pub next_cursor: Option<String>,
}
/// Requested amount of item detail for stored turns.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum StoredTurnItemsView {
/// Return turn metadata only.
NotLoaded,
/// Return display summary items for each turn.
#[default]
Summary,
/// Return every persisted item available for each turn.
Full,
}
/// Store-owned status for a persisted turn.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum StoredTurnStatus {
/// The turn completed normally.
Completed,
/// The turn was interrupted before normal completion.
Interrupted,
/// The turn failed.
Failed,
/// The turn is still in progress.
InProgress,
}
/// Store-owned error details for a failed persisted turn.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StoredTurnError {
/// User-visible error message.
pub message: String,
/// Optional additional detail for clients that expose expanded error context.
pub additional_details: Option<String>,
}
/// Parameters for listing turns within a stored thread.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListTurnsParams {
/// Thread id to read.
pub thread_id: ThreadId,
/// Whether archived threads are eligible.
pub include_archived: bool,
/// Opaque cursor returned by a previous list call.
pub cursor: Option<String>,
/// Maximum number of turns to return.
pub page_size: usize,
/// Sort direction requested by the caller.
pub sort_direction: SortDirection,
/// Requested amount of item detail for each returned turn.
pub items_view: StoredTurnItemsView,
}
/// Store-owned turn representation used by turn pagination APIs.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StoredTurn {
/// Turn id.
pub turn_id: String,
/// Persisted rollout items associated with this turn, according to `items_view`.
pub items: Vec<RolloutItem>,
/// Amount of item detail included in `items`.
pub items_view: StoredTurnItemsView,
/// Store-owned status for API layer projection.
pub status: StoredTurnStatus,
/// Error message when the turn failed.
pub error: Option<StoredTurnError>,
/// Unix timestamp (seconds) when the turn started.
pub started_at: Option<i64>,
/// Unix timestamp (seconds) when the turn completed.
pub completed_at: Option<i64>,
/// Duration between turn start and completion in milliseconds, if known.
pub duration_ms: Option<i64>,
}
/// A page of stored turns.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TurnPage {
/// Turns returned for this page.
pub turns: Vec<StoredTurn>,
/// Opaque cursor to continue listing.
pub next_cursor: Option<String>,
/// Opaque cursor for fetching in the opposite direction.
pub backwards_cursor: Option<String>,
}
/// Parameters for listing persisted items within a single turn.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ListItemsParams {
/// Thread id to read.
pub thread_id: ThreadId,
/// Turn id to hydrate.
pub turn_id: String,
/// Whether archived threads are eligible.
pub include_archived: bool,
/// Opaque cursor returned by a previous list call.
pub cursor: Option<String>,
/// Maximum number of items to return.
pub page_size: usize,
/// Sort direction requested by the caller.
pub sort_direction: SortDirection,
}
/// A page of persisted rollout items within a turn.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ItemPage {
/// Items returned for this page.
pub items: Vec<RolloutItem>,
/// Opaque cursor to continue listing.
pub next_cursor: Option<String>,
/// Opaque cursor for fetching in the opposite direction.
pub backwards_cursor: Option<String>,
}
/// Store-owned thread metadata used by list/read/resume responses.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StoredThread {