Skip to content

Commit b9abfde

Browse files
committed
feat: introduce GetConversationSummary RPC
1 parent c3a93cb commit b9abfde

File tree

4 files changed

+103
-8
lines changed

4 files changed

+103
-8
lines changed

codex-rs/app-server-protocol/src/protocol.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ client_request_definitions! {
140140
params: NewConversationParams,
141141
response: NewConversationResponse,
142142
},
143+
GetConversationSummary {
144+
params: GetConversationSummaryParams,
145+
response: GetConversationSummaryResponse,
146+
},
143147
/// List recorded Codex conversations (rollouts) with optional pagination and search.
144148
ListConversations {
145149
params: ListConversationsParams,
@@ -315,6 +319,18 @@ pub struct ResumeConversationResponse {
315319
pub initial_messages: Option<Vec<EventMsg>>,
316320
}
317321

322+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
323+
#[serde(rename_all = "camelCase")]
324+
pub struct GetConversationSummaryParams {
325+
pub rollout_path: PathBuf,
326+
}
327+
328+
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
329+
#[serde(rename_all = "camelCase")]
330+
pub struct GetConversationSummaryResponse {
331+
pub summary: ConversationSummary,
332+
}
333+
318334
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
319335
#[serde(rename_all = "camelCase")]
320336
pub struct ListConversationsParams {

codex-rs/app-server/src/codex_message_processor.rs

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use codex_app_server_protocol::ExecOneOffCommandResponse;
2121
use codex_app_server_protocol::FuzzyFileSearchParams;
2222
use codex_app_server_protocol::FuzzyFileSearchResponse;
2323
use codex_app_server_protocol::GetAccountRateLimitsResponse;
24+
use codex_app_server_protocol::GetConversationSummaryParams;
25+
use codex_app_server_protocol::GetConversationSummaryResponse;
2426
use codex_app_server_protocol::GetUserAgentResponse;
2527
use codex_app_server_protocol::GetUserSavedConfigResponse;
2628
use codex_app_server_protocol::GitDiffToRemoteResponse;
@@ -85,6 +87,7 @@ use codex_core::protocol::EventMsg;
8587
use codex_core::protocol::ExecApprovalRequestEvent;
8688
use codex_core::protocol::Op;
8789
use codex_core::protocol::ReviewDecision;
90+
use codex_core::read_head_records;
8891
use codex_login::ServerOptions as LoginServerOptions;
8992
use codex_login::ShutdownHandle;
9093
use codex_login::run_login_server;
@@ -98,6 +101,8 @@ use codex_protocol::user_input::UserInput as CoreInputItem;
98101
use codex_utils_json_to_toml::json_to_toml;
99102
use std::collections::HashMap;
100103
use std::ffi::OsStr;
104+
use std::io::Error as IoError;
105+
use std::path::Path;
101106
use std::path::PathBuf;
102107
use std::sync::Arc;
103108
use std::sync::atomic::AtomicBool;
@@ -170,6 +175,9 @@ impl CodexMessageProcessor {
170175
// created before processing any subsequent messages.
171176
self.process_new_conversation(request_id, params).await;
172177
}
178+
ClientRequest::GetConversationSummary { request_id, params } => {
179+
self.get_conversation_summary(request_id, params).await;
180+
}
173181
ClientRequest::ListConversations { request_id, params } => {
174182
self.handle_list_conversations(request_id, params).await;
175183
}
@@ -813,6 +821,39 @@ impl CodexMessageProcessor {
813821
}
814822
}
815823

824+
async fn get_conversation_summary(
825+
&self,
826+
request_id: RequestId,
827+
params: GetConversationSummaryParams,
828+
) {
829+
let GetConversationSummaryParams { rollout_path } = params;
830+
let path = if rollout_path.is_relative() {
831+
self.config.codex_home.join(&rollout_path)
832+
} else {
833+
rollout_path.clone()
834+
};
835+
let fallback_provider = self.config.model_provider_id.as_str();
836+
837+
match read_summary_from_rollout(&path, fallback_provider).await {
838+
Ok(summary) => {
839+
let response = GetConversationSummaryResponse { summary };
840+
self.outgoing.send_response(request_id, response).await;
841+
}
842+
Err(err) => {
843+
let error = JSONRPCErrorError {
844+
code: INTERNAL_ERROR_CODE,
845+
message: format!(
846+
"failed to load conversation summary from {}: {}",
847+
path.display(),
848+
err
849+
),
850+
data: None,
851+
};
852+
self.outgoing.send_error(request_id, error).await;
853+
}
854+
}
855+
}
856+
816857
async fn handle_list_conversations(
817858
&self,
818859
request_id: RequestId,
@@ -1644,6 +1685,34 @@ async fn on_exec_approval_response(
16441685
}
16451686
}
16461687

1688+
async fn read_summary_from_rollout(
1689+
path: &Path,
1690+
fallback_provider: &str,
1691+
) -> std::io::Result<ConversationSummary> {
1692+
let head = read_head_records(path, 1).await?;
1693+
1694+
let Some(first) = head.first() else {
1695+
return Err(IoError::other(format!(
1696+
"rollout at {} is empty",
1697+
path.display()
1698+
)));
1699+
};
1700+
1701+
serde_json::from_value::<SessionMeta>(first.clone()).map_err(|_| {
1702+
IoError::other(format!(
1703+
"rollout at {} does not start with session metadata",
1704+
path.display()
1705+
))
1706+
})?;
1707+
1708+
extract_conversation_summary(path.to_path_buf(), &head, fallback_provider).ok_or_else(|| {
1709+
IoError::other(format!(
1710+
"rollout at {} is missing a user message",
1711+
path.display()
1712+
))
1713+
})
1714+
}
1715+
16471716
fn extract_conversation_summary(
16481717
path: PathBuf,
16491718
head: &[serde_json::Value],
@@ -1730,14 +1799,15 @@ mod tests {
17301799
let summary =
17311800
extract_conversation_summary(path.clone(), &head, "test-provider").expect("summary");
17321801

1733-
assert_eq!(summary.conversation_id, conversation_id);
1734-
assert_eq!(
1735-
summary.timestamp,
1736-
Some("2025-09-05T16:53:11.850Z".to_string())
1737-
);
1738-
assert_eq!(summary.path, path);
1739-
assert_eq!(summary.preview, "Count to 5");
1740-
assert_eq!(summary.model_provider, "test-provider");
1802+
let expected = ConversationSummary {
1803+
conversation_id,
1804+
timestamp,
1805+
path,
1806+
preview: "Count to 5".to_string(),
1807+
model_provider: "test-provider".to_string(),
1808+
};
1809+
1810+
assert_eq!(summary, expected);
17411811
Ok(())
17421812
}
17431813
}

codex-rs/core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ pub use rollout::find_conversation_path_by_id_str;
7777
pub use rollout::list::ConversationItem;
7878
pub use rollout::list::ConversationsPage;
7979
pub use rollout::list::Cursor;
80+
pub use rollout::list::read_head_records;
8081
mod function_tool;
8182
mod state;
8283
mod tasks;

codex-rs/core/src/rollout/list.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,14 @@ async fn read_head_and_tail(
451451
Ok(summary)
452452
}
453453

454+
pub async fn read_head_records(
455+
path: &Path,
456+
head_limit: usize,
457+
) -> io::Result<Vec<serde_json::Value>> {
458+
let summary = read_head_and_tail(path, head_limit, 0).await?;
459+
Ok(summary.head)
460+
}
461+
454462
async fn read_tail_records(
455463
path: &Path,
456464
max_records: usize,

0 commit comments

Comments
 (0)