1use cellstate_core::{
7 AgentId, AgentStatus, ArtifactId, ArtifactType, NoteId, NoteType, ScopeId, TrajectoryId,
8 TrajectoryStatus, TurnId, TurnRole,
9};
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14#[derive(Debug, Clone, Serialize)]
20pub struct CreateTrajectoryRequest {
21 pub name: String,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub description: Option<String>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub parent_trajectory_id: Option<TrajectoryId>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub agent_id: Option<AgentId>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub metadata: Option<serde_json::Value>,
30}
31
32#[derive(Debug, Clone, Serialize)]
34pub struct UpdateTrajectoryRequest {
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub name: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub description: Option<String>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub status: Option<TrajectoryStatus>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub metadata: Option<serde_json::Value>,
43}
44
45#[derive(Debug, Clone, Deserialize)]
47pub struct TrajectoryResponse {
48 pub trajectory_id: TrajectoryId,
49 pub name: String,
50 pub description: Option<String>,
51 pub status: TrajectoryStatus,
52 pub parent_trajectory_id: Option<TrajectoryId>,
53 pub root_trajectory_id: Option<TrajectoryId>,
54 pub agent_id: Option<AgentId>,
55 pub created_at: DateTime<Utc>,
56 pub updated_at: DateTime<Utc>,
57 pub metadata: Option<serde_json::Value>,
58}
59
60#[derive(Debug, Clone, Serialize)]
66pub struct CreateScopeRequest {
67 pub trajectory_id: TrajectoryId,
68 pub name: String,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub description: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub parent_scope_id: Option<ScopeId>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub agent_id: Option<AgentId>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub token_budget: Option<i64>,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub metadata: Option<serde_json::Value>,
79}
80
81#[derive(Debug, Clone, Serialize)]
83pub struct UpdateScopeRequest {
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub name: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub description: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub status: Option<String>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub token_budget: Option<i64>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub metadata: Option<serde_json::Value>,
94}
95
96#[derive(Debug, Clone, Deserialize)]
98pub struct ScopeResponse {
99 pub scope_id: ScopeId,
100 pub trajectory_id: TrajectoryId,
101 pub name: String,
102 pub description: Option<String>,
103 pub status: String,
104 pub parent_scope_id: Option<ScopeId>,
105 pub agent_id: Option<AgentId>,
106 pub token_budget: Option<i64>,
107 pub tokens_used: i64,
108 pub created_at: DateTime<Utc>,
109 pub updated_at: DateTime<Utc>,
110 pub closed_at: Option<DateTime<Utc>>,
111 pub metadata: Option<serde_json::Value>,
112}
113
114#[derive(Debug, Clone, Serialize)]
120pub struct CreateArtifactRequest {
121 pub scope_id: ScopeId,
122 pub artifact_type: ArtifactType,
123 pub content: String,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub filename: Option<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub mime_type: Option<String>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub metadata: Option<serde_json::Value>,
130}
131
132#[derive(Debug, Clone, Serialize)]
134pub struct UpdateArtifactRequest {
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub content: Option<String>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub filename: Option<String>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub mime_type: Option<String>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub metadata: Option<serde_json::Value>,
143}
144
145#[derive(Debug, Clone, Deserialize)]
147pub struct ArtifactResponse {
148 pub artifact_id: ArtifactId,
149 pub scope_id: ScopeId,
150 pub artifact_type: ArtifactType,
151 pub content_hash: Option<String>,
152 pub content: Option<String>,
153 pub filename: Option<String>,
154 pub mime_type: Option<String>,
155 pub size_bytes: Option<i64>,
156 pub version: i32,
157 pub created_at: DateTime<Utc>,
158 pub updated_at: DateTime<Utc>,
159 pub metadata: Option<serde_json::Value>,
160}
161
162#[derive(Debug, Clone, Serialize)]
168pub struct CreateNoteRequest {
169 pub scope_id: ScopeId,
170 pub note_type: NoteType,
171 pub content: String,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub source_turn_id: Option<TurnId>,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub importance: Option<f64>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub metadata: Option<serde_json::Value>,
178}
179
180#[derive(Debug, Clone, Serialize)]
182pub struct UpdateNoteRequest {
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub content: Option<String>,
185 #[serde(skip_serializing_if = "Option::is_none")]
186 pub importance: Option<f64>,
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub metadata: Option<serde_json::Value>,
189}
190
191#[derive(Debug, Clone, Deserialize)]
193pub struct NoteResponse {
194 pub note_id: NoteId,
195 pub scope_id: ScopeId,
196 pub note_type: NoteType,
197 pub content: String,
198 pub source_turn_id: Option<TurnId>,
199 pub importance: Option<f64>,
200 pub version: i32,
201 pub created_at: DateTime<Utc>,
202 pub updated_at: DateTime<Utc>,
203 pub metadata: Option<serde_json::Value>,
204}
205
206#[derive(Debug, Clone, Serialize)]
212pub struct CreateTurnRequest {
213 pub scope_id: ScopeId,
214 pub role: TurnRole,
215 pub content: String,
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub agent_id: Option<AgentId>,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub tokens_input: Option<i64>,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub tokens_output: Option<i64>,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub model: Option<String>,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub metadata: Option<serde_json::Value>,
226}
227
228#[derive(Debug, Clone, Deserialize)]
230pub struct TurnResponse {
231 pub turn_id: TurnId,
232 pub scope_id: ScopeId,
233 pub role: TurnRole,
234 pub content: String,
235 pub agent_id: Option<AgentId>,
236 pub tokens_input: Option<i64>,
237 pub tokens_output: Option<i64>,
238 pub model: Option<String>,
239 pub created_at: DateTime<Utc>,
240 pub metadata: Option<serde_json::Value>,
241}
242
243#[derive(Debug, Clone, Serialize)]
249pub struct CreateAgentRequest {
250 pub name: String,
251 #[serde(skip_serializing_if = "Option::is_none")]
252 pub description: Option<String>,
253 #[serde(skip_serializing_if = "Option::is_none")]
254 pub model: Option<String>,
255 #[serde(skip_serializing_if = "Option::is_none")]
256 pub system_prompt: Option<String>,
257 #[serde(skip_serializing_if = "Option::is_none")]
258 pub metadata: Option<serde_json::Value>,
259}
260
261#[derive(Debug, Clone, Deserialize)]
263pub struct AgentResponse {
264 pub agent_id: AgentId,
265 pub name: String,
266 pub description: Option<String>,
267 pub status: AgentStatus,
268 pub model: Option<String>,
269 pub system_prompt: Option<String>,
270 pub created_at: DateTime<Utc>,
271 pub updated_at: DateTime<Utc>,
272 pub metadata: Option<serde_json::Value>,
273}
274
275#[derive(Debug, Clone, Serialize)]
281pub struct CommitMemoryRequest {
282 pub trajectory_id: TrajectoryId,
283 pub scope_id: ScopeId,
284 pub query: String,
285 pub response: String,
286 pub mode: String,
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub agent_id: Option<AgentId>,
289 #[serde(skip_serializing_if = "Option::is_none")]
290 pub reasoning_trace: Option<serde_json::Value>,
291 #[serde(skip_serializing_if = "Option::is_none")]
292 pub tools_invoked: Option<Vec<String>>,
293 #[serde(skip_serializing_if = "Option::is_none")]
294 pub tokens_input: Option<i64>,
295 #[serde(skip_serializing_if = "Option::is_none")]
296 pub tokens_output: Option<i64>,
297}
298
299#[derive(Debug, Clone, Deserialize)]
301pub struct CommitMemoryResponse {
302 pub commit_id: Uuid,
303 pub trajectory_id: TrajectoryId,
304 pub scope_id: ScopeId,
305 pub created_at: DateTime<Utc>,
306}
307
308#[derive(Debug, Clone, Serialize)]
314pub struct RecallRequest {
315 pub trajectory_id: TrajectoryId,
316 #[serde(skip_serializing_if = "Option::is_none")]
317 pub scope_id: Option<ScopeId>,
318 #[serde(skip_serializing_if = "Option::is_none")]
319 pub query: Option<String>,
320 #[serde(skip_serializing_if = "Option::is_none")]
321 pub limit: Option<usize>,
322}
323
324#[derive(Debug, Clone, Deserialize)]
326pub struct RecallResult {
327 pub commit_id: Uuid,
328 pub query: String,
329 pub response: String,
330 pub mode: String,
331 pub relevance_score: Option<f64>,
332 pub created_at: DateTime<Utc>,
333}
334
335#[derive(Debug, Clone, Deserialize)]
337pub struct RecallResponse {
338 pub results: Vec<RecallResult>,
339 pub total_count: usize,
340}
341
342#[derive(Debug, Clone, Serialize)]
348pub struct AssembleContextRequest {
349 pub trajectory_id: TrajectoryId,
350 pub scope_id: ScopeId,
351 #[serde(skip_serializing_if = "Option::is_none")]
352 pub user_input: Option<String>,
353 #[serde(skip_serializing_if = "Option::is_none")]
354 pub max_tokens: Option<usize>,
355}
356
357#[derive(Debug, Clone, Deserialize)]
359pub struct AssembleContextResponse {
360 pub segments: Vec<ContextSegment>,
361 pub total_tokens: usize,
362 pub budget_remaining: usize,
363}
364
365#[derive(Debug, Clone, Deserialize)]
367pub struct ContextSegment {
368 pub name: String,
369 pub content: String,
370 pub token_count: usize,
371}
372
373#[derive(Debug, Clone, Deserialize)]
379pub struct ListResponse<T> {
380 pub data: Vec<T>,
381 pub total: usize,
382 #[serde(skip_serializing_if = "Option::is_none")]
383 pub next_cursor: Option<String>,
384}
385
386#[derive(Debug, Clone, Default)]
388pub struct ListParams {
389 pub limit: Option<usize>,
390 pub offset: Option<usize>,
391 pub cursor: Option<String>,
392}
393
394impl ListParams {
395 pub(crate) fn to_query_pairs(&self) -> Vec<(&str, String)> {
397 let mut pairs = Vec::new();
398 if let Some(limit) = self.limit {
399 pairs.push(("limit", limit.to_string()));
400 }
401 if let Some(offset) = self.offset {
402 pairs.push(("offset", offset.to_string()));
403 }
404 if let Some(ref cursor) = self.cursor {
405 pairs.push(("cursor", cursor.clone()));
406 }
407 pairs
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414 use cellstate_core::EntityIdType;
415 use serde_json::Value;
416
417 #[test]
418 fn create_trajectory_request_serializes_snake_case() {
419 let req = CreateTrajectoryRequest {
420 name: "t".to_string(),
421 description: None,
422 parent_trajectory_id: Some(TrajectoryId::new(Uuid::now_v7())),
423 agent_id: None,
424 metadata: None,
425 };
426
427 let value = serde_json::to_value(req).expect("serialize request");
428 let obj = value.as_object().expect("request json object");
429 assert!(
430 obj.contains_key("parent_trajectory_id"),
431 "expected snake_case field"
432 );
433 assert!(
434 !obj.contains_key("parentTrajectoryId"),
435 "camelCase field should not be emitted"
436 );
437 }
438
439 #[test]
440 fn create_scope_request_serializes_snake_case() {
441 let req = CreateScopeRequest {
442 trajectory_id: TrajectoryId::new(Uuid::now_v7()),
443 name: "scope".to_string(),
444 description: None,
445 parent_scope_id: None,
446 agent_id: None,
447 token_budget: Some(1024),
448 metadata: None,
449 };
450
451 let value = serde_json::to_value(req).expect("serialize request");
452 let obj = value.as_object().expect("request json object");
453 assert!(obj.contains_key("trajectory_id"));
454 assert!(obj.contains_key("token_budget"));
455 assert!(!obj.contains_key("trajectoryId"));
456 assert!(!obj.contains_key("tokenBudget"));
457 }
458
459 #[test]
460 fn contract_fixture_create_trajectory_request_is_snake_case() {
461 let fixture: Value = serde_json::from_str(include_str!(
462 "../../../tests/contracts/fixtures/create_trajectory_request.json"
463 ))
464 .expect("valid create_trajectory_request fixture");
465
466 let obj = fixture.as_object().expect("fixture json object");
467 assert!(obj.contains_key("name"));
468 assert!(obj.contains_key("description"));
469 assert!(!obj.contains_key("parentTrajectoryId"));
470 assert!(!obj.contains_key("agentId"));
471 }
472
473 #[test]
474 fn contract_fixture_trajectory_response_deserializes() {
475 let fixture: Value = serde_json::from_str(include_str!(
476 "../../../tests/contracts/fixtures/trajectory_response.json"
477 ))
478 .expect("valid trajectory_response fixture");
479
480 let parsed: TrajectoryResponse =
481 serde_json::from_value(fixture).expect("trajectory response should deserialize");
482 assert_eq!(parsed.status, TrajectoryStatus::Active);
483 assert_eq!(parsed.name, "contract-trajectory");
484 }
485}