feat(app-server): thread/turns/items/list -> thread/items/list (#29705)

## Description

Rename the experimental app-server item pagination API from
`thread/turns/items/list` to `thread/items/list` and make `turnId`
optional. Clients can now page persisted items across a thread, or still
filter to one turn when needed.

## What changed

- Rename the request/response protocol types and JSON-RPC method to
`ThreadItemsList*` / `thread/items/list`.
- Pass optional `turnId` through to `ThreadStore::list_items`.
- Update app-server docs and focused protocol/app-server tests.

## Validation

- `just test -p codex-app-server-protocol thread_items_list_round_trips`
- `just test -p codex-app-server thread_items_list_returns_unsupported`
This commit is contained in:
Owen Lin
2026-06-23 13:57:08 -07:00
committed by GitHub
Unverified
parent 66f0220c56
commit 1882719b30
9 changed files with 132 additions and 38 deletions
@@ -641,12 +641,12 @@ client_request_definitions! {
serialization: None,
response: v2::ThreadTurnsListResponse,
},
#[experimental("thread/turns/items/list")]
ThreadTurnsItemsList => "thread/turns/items/list" {
params: v2::ThreadTurnsItemsListParams,
#[experimental("thread/items/list")]
ThreadItemsList => "thread/items/list" {
params: v2::ThreadItemsListParams,
// Explicitly concurrent: this primarily reads append-only rollout storage.
serialization: None,
response: v2::ThreadTurnsItemsListResponse,
response: v2::ThreadItemsListResponse,
},
/// Append raw Responses API items to the thread history without starting a user turn.
ThreadInjectItems => "thread/inject_items" {
@@ -2090,17 +2090,17 @@ mod tests {
};
assert_eq!(thread_turns_list.serialization_scope(), None);
let thread_turns_items_list = ClientRequest::ThreadTurnsItemsList {
let thread_items_list = ClientRequest::ThreadItemsList {
request_id: request_id(),
params: v2::ThreadTurnsItemsListParams {
params: v2::ThreadItemsListParams {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
turn_id: None,
cursor: None,
limit: None,
sort_direction: None,
},
};
assert_eq!(thread_turns_items_list.serialization_scope(), None);
assert_eq!(thread_items_list.serialization_scope(), None);
let mcp_resource_read = ClientRequest::McpResourceRead {
request_id: request_id(),
@@ -224,10 +224,10 @@ fn thread_resume_response_round_trips_initial_turns_page() {
}
#[test]
fn thread_turns_items_list_round_trips() {
let params = ThreadTurnsItemsListParams {
fn thread_items_list_round_trips() {
let params = ThreadItemsListParams {
thread_id: "thr_123".to_string(),
turn_id: "turn_456".to_string(),
turn_id: Some("turn_456".to_string()),
cursor: Some("cursor_1".to_string()),
limit: Some(50),
sort_direction: Some(SortDirection::Asc),
@@ -243,7 +243,7 @@ fn thread_turns_items_list_round_trips() {
"sortDirection": "asc",
})
);
let response = ThreadTurnsItemsListResponse {
let response = ThreadItemsListResponse {
data: vec![ThreadItem::ContextCompaction {
id: "item_1".to_string(),
}],
@@ -259,6 +259,32 @@ fn thread_turns_items_list_round_trips() {
"backwardsCursor": "cursor_0",
})
);
let params_without_turn = ThreadItemsListParams {
thread_id: "thr_123".to_string(),
turn_id: None,
cursor: None,
limit: None,
sort_direction: None,
};
assert_eq!(
serde_json::to_value(&params_without_turn).expect("serialize params without turn"),
json!({
"threadId": "thr_123",
"turnId": null,
"cursor": null,
"limit": null,
"sortDirection": null,
})
);
assert_eq!(
serde_json::from_value::<ThreadItemsListParams>(json!({
"threadId": "thr_123",
}))
.expect("deserialize params without turn"),
params_without_turn
);
}
#[test]
@@ -1316,9 +1316,11 @@ pub struct ThreadTurnsListResponse {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadTurnsItemsListParams {
pub struct ThreadItemsListParams {
pub thread_id: String,
pub turn_id: String,
/// Optional turn id to filter by. When omitted, returns items across the thread.
#[ts(optional = nullable)]
pub turn_id: Option<String>,
/// Opaque cursor to pass to the next call to continue after the last item.
#[ts(optional = nullable)]
pub cursor: Option<String>,
@@ -1333,7 +1335,7 @@ pub struct ThreadTurnsItemsListParams {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadTurnsItemsListResponse {
pub struct ThreadItemsListResponse {
pub data: Vec<ThreadItem>,
/// Opaque cursor to pass to the next call to continue after the last item.
/// if None, there are no more items to return.