Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions codex-rs/codex-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,13 @@ The public interface of this crate is intentionally small and uniform:
- Output: `Vec<ResponseItem>`.
- `CompactClient::compact_input(&CompactionInput, extra_headers)` wraps the JSON encoding and retry/telemetry wiring.

- **Memory trace summarize endpoint**
- Input: `MemoryTraceSummarizeInput` (re-exported as `codex_api::MemoryTraceSummarizeInput`):
- `model: String`.
- `traces: Vec<MemoryTrace>`.
- `MemoryTrace` includes `id`, `metadata.source_path`, and normalized `items`.
- `reasoning: Option<Reasoning>`.
- Output: `Vec<MemoryTraceSummaryOutput>`.
- `MemoriesClient::trace_summarize_input(&MemoryTraceSummarizeInput, extra_headers)` wraps JSON encoding and retry/telemetry wiring.

All HTTP details (URLs, headers, retry/backoff policies, SSE framing) are encapsulated in `codex-api` and `codex-client`. Callers construct prompts/inputs using protocol types and work with typed streams of `ResponseEvent` or compacted `ResponseItem` values.
28 changes: 28 additions & 0 deletions codex-rs/codex-api/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::TokenUsage;
use futures::Stream;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
use std::pin::Pin;
Expand Down Expand Up @@ -37,6 +38,33 @@ pub struct CompactionInput<'a> {
pub instructions: &'a str,
}

/// Canonical input payload for the memory trace summarize endpoint.
#[derive(Debug, Clone, Serialize)]
pub struct MemoryTraceSummarizeInput {
pub model: String,
pub traces: Vec<MemoryTrace>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<Reasoning>,
}

#[derive(Debug, Clone, Serialize)]
pub struct MemoryTrace {
pub id: String,
pub metadata: MemoryTraceMetadata,
pub items: Vec<Value>,
}

#[derive(Debug, Clone, Serialize)]
pub struct MemoryTraceMetadata {
pub source_path: String,
}

#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct MemoryTraceSummaryOutput {
pub trace_summary: String,
pub memory_summary: String,
}

#[derive(Debug)]
pub enum ResponseEvent {
Created,
Expand Down
108 changes: 108 additions & 0 deletions codex-rs/codex-api/src/endpoint/memories.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use crate::auth::AuthProvider;
use crate::common::MemoryTraceSummarizeInput;
use crate::common::MemoryTraceSummaryOutput;
use crate::endpoint::session::EndpointSession;
use crate::error::ApiError;
use crate::provider::Provider;
use codex_client::HttpTransport;
use codex_client::RequestTelemetry;
use http::HeaderMap;
use http::Method;
use serde::Deserialize;
use serde_json::to_value;
use std::sync::Arc;

pub struct MemoriesClient<T: HttpTransport, A: AuthProvider> {
session: EndpointSession<T, A>,
}

impl<T: HttpTransport, A: AuthProvider> MemoriesClient<T, A> {
pub fn new(transport: T, provider: Provider, auth: A) -> Self {
Self {
session: EndpointSession::new(transport, provider, auth),
}
}

pub fn with_telemetry(self, request: Option<Arc<dyn RequestTelemetry>>) -> Self {
Self {
session: self.session.with_request_telemetry(request),
}
}

fn path() -> &'static str {
"memories/trace_summarize"
}

pub async fn trace_summarize(
&self,
body: serde_json::Value,
extra_headers: HeaderMap,
) -> Result<Vec<MemoryTraceSummaryOutput>, ApiError> {
let resp = self
.session
.execute(Method::POST, Self::path(), extra_headers, Some(body))
.await?;
let parsed: TraceSummarizeResponse =
serde_json::from_slice(&resp.body).map_err(|e| ApiError::Stream(e.to_string()))?;
Ok(parsed.output)
}

pub async fn trace_summarize_input(
&self,
input: &MemoryTraceSummarizeInput,
extra_headers: HeaderMap,
) -> Result<Vec<MemoryTraceSummaryOutput>, ApiError> {
let body = to_value(input).map_err(|e| {
ApiError::Stream(format!(
"failed to encode memory trace summarize input: {e}"
))
})?;
self.trace_summarize(body, extra_headers).await
}
}

#[derive(Debug, Deserialize)]
struct TraceSummarizeResponse {
output: Vec<MemoryTraceSummaryOutput>,
}

#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use codex_client::Request;
use codex_client::Response;
use codex_client::StreamResponse;
use codex_client::TransportError;

#[derive(Clone, Default)]
struct DummyTransport;

#[async_trait]
impl HttpTransport for DummyTransport {
async fn execute(&self, _req: Request) -> Result<Response, TransportError> {
Err(TransportError::Build("execute should not run".to_string()))
}

async fn stream(&self, _req: Request) -> Result<StreamResponse, TransportError> {
Err(TransportError::Build("stream should not run".to_string()))
}
}

#[derive(Clone, Default)]
struct DummyAuth;

impl AuthProvider for DummyAuth {
fn bearer_token(&self) -> Option<String> {
None
}
}

#[test]
fn path_is_memories_trace_summarize() {
assert_eq!(
MemoriesClient::<DummyTransport, DummyAuth>::path(),
"memories/trace_summarize"
);
}
}
1 change: 1 addition & 0 deletions codex-rs/codex-api/src/endpoint/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod aggregate;
pub mod compact;
pub mod memories;
pub mod models;
pub mod responses;
pub mod responses_websocket;
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/codex-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ pub use codex_client::TransportError;

pub use crate::auth::AuthProvider;
pub use crate::common::CompactionInput;
pub use crate::common::MemoryTrace;
pub use crate::common::MemoryTraceMetadata;
pub use crate::common::MemoryTraceSummarizeInput;
pub use crate::common::MemoryTraceSummaryOutput;
pub use crate::common::Prompt;
pub use crate::common::ResponseAppendWsRequest;
pub use crate::common::ResponseCreateWsRequest;
Expand All @@ -24,6 +28,7 @@ pub use crate::common::ResponsesApiRequest;
pub use crate::common::create_text_param_for_request;
pub use crate::endpoint::aggregate::AggregateStreamExt;
pub use crate::endpoint::compact::CompactClient;
pub use crate::endpoint::memories::MemoriesClient;
pub use crate::endpoint::models::ModelsClient;
pub use crate::endpoint::responses::ResponsesClient;
pub use crate::endpoint::responses::ResponsesOptions;
Expand Down
58 changes: 54 additions & 4 deletions codex-rs/core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ use crate::api_bridge::map_api_error;
use crate::auth::UnauthorizedRecovery;
use codex_api::CompactClient as ApiCompactClient;
use codex_api::CompactionInput as ApiCompactionInput;
use codex_api::MemoriesClient as ApiMemoriesClient;
use codex_api::MemoryTrace as ApiMemoryTrace;
use codex_api::MemoryTraceSummarizeInput as ApiMemoryTraceSummarizeInput;
use codex_api::MemoryTraceSummaryOutput as ApiMemoryTraceSummaryOutput;
use codex_api::Prompt as ApiPrompt;
use codex_api::RequestTelemetry;
use codex_api::ReqwestTransport;
Expand Down Expand Up @@ -238,6 +242,55 @@ impl ModelClient {
instructions: &instructions,
};

let extra_headers = self.build_subagent_headers();
client
.compact_input(&payload, extra_headers)
.await
.map_err(map_api_error)
}

/// Builds memory summaries for each provided normalized trace.
///
/// This is a unary call (no streaming) to `/v1/memories/trace_summarize`.
pub async fn summarize_memory_traces(
&self,
traces: Vec<ApiMemoryTrace>,
) -> Result<Vec<ApiMemoryTraceSummaryOutput>> {
if traces.is_empty() {
return Ok(Vec::new());
}

let auth_manager = self.state.auth_manager.clone();
let auth = match auth_manager.as_ref() {
Some(manager) => manager.auth().await,
None => None,
};
let api_provider = self
.state
.provider
.to_api_provider(auth.as_ref().map(CodexAuth::internal_auth_mode))?;
let api_auth = auth_provider_from_auth(auth, &self.state.provider)?;
let transport = ReqwestTransport::new(build_reqwest_client());
let request_telemetry = self.build_request_telemetry();
let client = ApiMemoriesClient::new(transport, api_provider, api_auth)
.with_telemetry(Some(request_telemetry));

let payload = ApiMemoryTraceSummarizeInput {
model: self.state.model_info.slug.clone(),
traces,
reasoning: self.state.effort.map(|effort| Reasoning {
effort: Some(effort),
summary: None,
}),
};

client
.trace_summarize_input(&payload, self.build_subagent_headers())
.await
.map_err(map_api_error)
}

fn build_subagent_headers(&self) -> ApiHeaderMap {
let mut extra_headers = ApiHeaderMap::new();
if let SessionSource::SubAgent(sub) = &self.state.session_source {
let subagent = match sub {
Expand All @@ -250,10 +303,7 @@ impl ModelClient {
extra_headers.insert("x-openai-subagent", val);
}
}
client
.compact_input(&payload, extra_headers)
.await
.map_err(map_api_error)
extra_headers
}
}

Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,5 @@ pub use codex_protocol::models::ResponseItem;
pub use compact::content_items_to_text;
pub use event_mapping::parse_turn_item;
pub mod compact;
pub mod memory_trace;
pub mod otel_init;
Loading
Loading