1use crate::{
2 HttpBody, WireResponse,
3 adapt::{JsonInvocation, invoke_json},
4 capability::Upstream,
5 codec::{self, CodecLimits},
6 openai::memory::{MemorySummarizeOutput, RawMemory},
7 transform::{
8 TransformError, TransformErrorKind,
9 memory::{self, MemoryDialectRequest},
10 },
11 wire::DeclaredFields,
12};
13
14#[derive(Debug, Clone, Copy)]
15pub struct MemoryLimits {
16 pub max_bytes: u64,
17 pub codec: CodecLimits,
18}
19
20#[derive(Debug, Clone, Copy)]
21pub enum MemoryDialect {
22 Claude,
23 Gemini,
24 OpenAiChat,
25 OpenAiResponses,
26}
27
28#[derive(Debug)]
29pub enum MemoryFailure {
30 Transform(TransformError),
31 Rejected(Box<WireResponse<HttpBody>>),
32}
33
34#[derive(Debug)]
35pub struct MemoryError {
36 pub attempted_calls: usize,
37 pub completed_traces: usize,
38 pub trace_id: Option<String>,
39 pub failure: MemoryFailure,
40}
41
42impl MemoryError {
43 fn at(error: TransformError, calls: usize, done: usize, id: Option<String>) -> Self {
44 Self {
45 attempted_calls: calls,
46 completed_traces: done,
47 trace_id: id,
48 failure: MemoryFailure::Transform(error),
49 }
50 }
51 pub fn kind(&self) -> TransformErrorKind {
52 match &self.failure {
53 MemoryFailure::Transform(e) => e.kind(),
54 MemoryFailure::Rejected(_) => TransformErrorKind::InvalidResult,
55 }
56 }
57}
58
59impl From<TransformError> for MemoryError {
60 fn from(e: TransformError) -> Self {
61 Self::at(e, 0, 0, None)
62 }
63}
64
65pub async fn summarize<U: Upstream>(
66 upstream: &U,
67 target: &U::Target,
68 dialect: MemoryDialect,
69 traces: Vec<RawMemory>,
70 model: impl Into<String>,
71 max_tokens: i64,
72 limits: MemoryLimits,
73) -> Result<Vec<MemorySummarizeOutput>, MemoryError> {
74 summarize_with_reasoning(
75 upstream, target, dialect, traces, model, max_tokens, None, limits,
76 )
77 .await
78}
79
80#[allow(clippy::too_many_arguments)]
81pub async fn summarize_with_reasoning<U: Upstream>(
82 upstream: &U,
83 target: &U::Target,
84 dialect: MemoryDialect,
85 traces: Vec<RawMemory>,
86 model: impl Into<String>,
87 max_tokens: i64,
88 reasoning: Option<crate::openai::codex::Reasoning>,
89 limits: MemoryLimits,
90) -> Result<Vec<MemorySummarizeOutput>, MemoryError> {
91 let model = model.into();
92 if model.trim().is_empty() || max_tokens <= 0 {
93 return Err(TransformError::shape(
94 "memory.target",
95 "nonempty model and positive token budget required",
96 )
97 .into());
98 }
99
100 let reasoning = reasoning.into_declared();
101 let mut planned = Vec::new();
102 let mut ids = std::collections::BTreeSet::new();
103 let mut total = 0u64;
104 let mut retained = 0u64;
105 let mut bounds = limits.codec;
106 bounds.max_body_bytes = bounds.max_body_bytes.min(upstream.limits().write_bytes);
107 bounds.max_value_bytes = bounds.max_value_bytes.min(bounds.max_body_bytes);
108 bounds.max_buffer_bytes = bounds.max_buffer_bytes.min(bounds.max_body_bytes);
109 for trace in traces {
112 let trace = trace.into_declared();
113
114 if !ids.insert(trace.id.clone()) {
115 return Err(TransformError::shape("trace.id", "duplicate trace identity").into());
116 }
117 let remaining = limits
118 .max_bytes
119 .checked_sub(total)
120 .ok_or_else(|| limit("max_bytes"))?;
121 let mut trace_bounds = bounds;
122 trace_bounds.max_value_bytes = trace_bounds.max_value_bytes.min(remaining);
123 trace_bounds.max_body_bytes = trace_bounds.max_body_bytes.min(remaining);
124 let payload = memory::trace_payload(&trace, trace_bounds)?;
125 total = total
126 .checked_add(payload.len() as u64)
127 .ok_or_else(|| limit("max_bytes"))?;
128 let request = match dialect {
129 MemoryDialect::Claude => {
130 memory::build_claude(payload, model.clone(), max_tokens, reasoning.as_ref())?
131 }
132 MemoryDialect::Gemini => {
133 memory::build_gemini(payload, model.clone(), max_tokens, reasoning.as_ref())?
134 }
135 MemoryDialect::OpenAiChat => {
136 memory::build_openai_chat(payload, model.clone(), max_tokens, reasoning.as_ref())?
137 }
138 MemoryDialect::OpenAiResponses => memory::build_openai_responses(
139 payload,
140 model.clone(),
141 max_tokens,
142 reasoning.as_ref(),
143 )?,
144 };
145 let mut request_bounds = bounds;
146 let remaining = limits
147 .max_bytes
148 .checked_sub(retained)
149 .ok_or_else(|| limit("max_bytes"))?;
150 request_bounds.max_value_bytes = request_bounds.max_value_bytes.min(remaining);
151 request_bounds.max_buffer_bytes = request_bounds.max_buffer_bytes.min(remaining);
152 request_bounds.max_body_bytes = request_bounds.max_body_bytes.min(remaining);
153 let encoded = match &request {
154 MemoryDialectRequest::Claude(v) => codec::encode_json(&v.body, request_bounds),
155 MemoryDialectRequest::Gemini(v) => codec::encode_json(&v.body, request_bounds),
156 MemoryDialectRequest::OpenAiChat(v) => codec::encode_json(&v.body, request_bounds),
157 MemoryDialectRequest::OpenAiResponses(v) => codec::encode_json(&v.body, request_bounds),
158 }
159 .map_err(|e| {
160 TransformError::new(
161 if e.kind() == codec::CodecErrorKind::Limit {
162 TransformErrorKind::Limit
163 } else {
164 TransformErrorKind::InvalidInput
165 },
166 "memory.request",
167 e.to_string(),
168 )
169 })?;
170 retained = retained
173 .checked_add(encoded.len() as u64)
174 .filter(|n| *n <= limits.max_bytes)
175 .ok_or_else(|| limit("max_bytes"))?;
176 planned.push((trace.id, request));
177 }
178 let mut output = Vec::new();
179 let mut output_bytes = 0u64;
180 for (index, (id, request)) in planned.into_iter().enumerate() {
181 let text = match request {
182 MemoryDialectRequest::Claude(request) => finish(
183 invoke_json(upstream, target, request, limits.codec).await,
184 memory::claude_text,
185 index,
186 output.len(),
187 &id,
188 )?,
189 MemoryDialectRequest::Gemini(request) => finish(
190 invoke_json(upstream, target, request, limits.codec).await,
191 memory::gemini_text,
192 index,
193 output.len(),
194 &id,
195 )?,
196 MemoryDialectRequest::OpenAiChat(request) => finish(
197 invoke_json(upstream, target, request, limits.codec).await,
198 memory::chat_text,
199 index,
200 output.len(),
201 &id,
202 )?,
203 MemoryDialectRequest::OpenAiResponses(request) => finish(
204 invoke_json(upstream, target, request, limits.codec).await,
205 memory::responses_text,
206 index,
207 output.len(),
208 &id,
209 )?,
210 };
211 let (trace, memory) = memory::parse_output(&text, limits.codec)
212 .map_err(|e| MemoryError::at(e, index + 1, output.len(), Some(id.clone())))?;
213 let value = MemorySummarizeOutput::builder(trace, memory).build();
214 let bytes = codec::encode_json(&value, limits.codec).map_err(|e| {
215 MemoryError::at(
216 TransformError::new(TransformErrorKind::Limit, "memory.output", e.to_string()),
217 index + 1,
218 output.len(),
219 Some(id.clone()),
220 )
221 })?;
222 output_bytes = output_bytes
223 .checked_add(bytes.len() as u64)
224 .filter(|v| *v <= limits.max_bytes)
225 .ok_or_else(|| {
226 MemoryError::at(limit("max_bytes"), index + 1, output.len(), Some(id))
227 })?;
228 output.push(value);
229 }
230 Ok(output)
231}
232
233fn finish<T>(
234 result: Result<JsonInvocation<T>, TransformError>,
235 extract: fn(T) -> Result<String, TransformError>,
236 index: usize,
237 completed: usize,
238 id: &str,
239) -> Result<String, MemoryError> {
240 match result.map_err(|e| MemoryError::at(e, index + 1, completed, Some(id.into())))? {
241 JsonInvocation::Success(response) => extract(response.body)
242 .map_err(|e| MemoryError::at(e, index + 1, completed, Some(id.into()))),
243 JsonInvocation::Rejected(response) => Err(MemoryError {
244 attempted_calls: index + 1,
245 completed_traces: completed,
246 trace_id: Some(id.into()),
247 failure: MemoryFailure::Rejected(Box::new(response)),
248 }),
249 }
250}
251
252fn limit(field: &str) -> TransformError {
253 TransformError::new(
254 TransformErrorKind::Limit,
255 format!("memory.{field}"),
256 "memory operation bound exceeded",
257 )
258}