cellstate/
types.rs

1//! Request and response wire types for the CELLSTATE REST API.
2//!
3//! These structs match the JSON payloads accepted and returned by the API.
4//! All fields use `snake_case` serialization to match the API wire format.
5
6use 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// ============================================================================
15// TRAJECTORY
16// ============================================================================
17
18/// Create a new trajectory.
19#[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/// Update an existing trajectory.
33#[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/// Trajectory response from the API.
46#[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// ============================================================================
61// SCOPE
62// ============================================================================
63
64/// Create a new scope within a trajectory.
65#[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/// Update an existing scope.
82#[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/// Scope response from the API.
97#[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// ============================================================================
115// ARTIFACT
116// ============================================================================
117
118/// Create a new artifact.
119#[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/// Update an existing artifact.
133#[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/// Artifact response from the API.
146#[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// ============================================================================
163// NOTE
164// ============================================================================
165
166/// Create a new note (extraction).
167#[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/// Update an existing note.
181#[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/// Note response from the API.
192#[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// ============================================================================
207// TURN
208// ============================================================================
209
210/// Create a new turn (conversation message).
211#[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/// Turn response from the API.
229#[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// ============================================================================
244// AGENT
245// ============================================================================
246
247/// Create a new agent.
248#[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/// Agent response from the API.
262#[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// ============================================================================
276// MEMORY COMMIT
277// ============================================================================
278
279/// Request to commit a memory interaction.
280#[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/// Memory commit response.
300#[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// ============================================================================
309// RECALL
310// ============================================================================
311
312/// Request to recall previous interactions.
313#[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/// Single recall result.
325#[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/// Response for recall operations.
336#[derive(Debug, Clone, Deserialize)]
337pub struct RecallResponse {
338    pub results: Vec<RecallResult>,
339    pub total_count: usize,
340}
341
342// ============================================================================
343// CONTEXT ASSEMBLY
344// ============================================================================
345
346/// Request to assemble a context window.
347#[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/// Assembled context window response.
358#[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/// Single segment within an assembled context.
366#[derive(Debug, Clone, Deserialize)]
367pub struct ContextSegment {
368    pub name: String,
369    pub content: String,
370    pub token_count: usize,
371}
372
373// ============================================================================
374// LIST PAGINATION
375// ============================================================================
376
377/// Paginated list response wrapper.
378#[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/// Pagination parameters.
387#[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    /// Build query string parameters.
396    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}