Skip to main content

agentbridge/
context.rs

1use serde::{Deserialize, Serialize};
2
3/// A portable snapshot of a coding-agent session: conversation history,
4/// code context, and any generated artifacts. Used for handoff and relay.
5#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
6pub struct ContextPacket {
7    /// Unique packet ID (UUID v4).
8    pub id: String,
9    /// Human-readable name of the agent that produced this snapshot.
10    pub source_agent: String,
11    /// ISO 8601 creation timestamp.
12    pub created_at: String,
13    pub conversation: Vec<ConversationMessage>,
14    pub code_context: CodeContext,
15    pub artifacts: Vec<ArtifactPayload>,
16}
17
18#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
19pub struct ConversationMessage {
20    /// "user", "assistant", or "system".
21    pub role: String,
22    pub content: String,
23}
24
25#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
26pub struct CodeContext {
27    pub files: Vec<FileSnapshot>,
28    pub git_diff: Option<String>,
29    /// Path of the file currently open / in focus.
30    pub active_file: Option<String>,
31    /// Absolute path of the project root.
32    pub project_root: Option<String>,
33}
34
35#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
36pub struct FileSnapshot {
37    pub path: String,
38    pub content: String,
39}
40
41/// A named text artifact (generated code, suggestion, patch, etc.).
42#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
43pub struct ArtifactPayload {
44    pub name: String,
45    pub content: String,
46    /// MIME type, e.g. "text/x-rust", "text/plain".
47    pub media_type: String,
48}
49
50impl ContextPacket {
51    pub fn new(source_agent: impl Into<String>) -> Self {
52        Self {
53            id: uuid::Uuid::new_v4().to_string(),
54            source_agent: source_agent.into(),
55            created_at: chrono_now(),
56            conversation: Vec::new(),
57            code_context: CodeContext::default(),
58            artifacts: Vec::new(),
59        }
60    }
61
62    pub fn to_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
63        serde_json::to_vec(self)
64    }
65
66    pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
67        serde_json::from_slice(bytes)
68    }
69}
70
71fn chrono_now() -> String {
72    // Use std::time to avoid pulling in chrono/time as a required dep.
73    use std::time::{SystemTime, UNIX_EPOCH};
74    let secs = SystemTime::now()
75        .duration_since(UNIX_EPOCH)
76        .map(|d| d.as_secs())
77        .unwrap_or(0);
78    format!("{secs}")
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn round_trip_serialization() {
87        let mut pkt = ContextPacket::new("claude-code");
88        pkt.conversation.push(ConversationMessage {
89            role: "user".to_string(),
90            content: "write a parser".to_string(),
91        });
92        pkt.code_context.files.push(FileSnapshot {
93            path: "src/lib.rs".to_string(),
94            content: "fn main() {}".to_string(),
95        });
96        pkt.artifacts.push(ArtifactPayload {
97            name: "parser.rs".to_string(),
98            content: "fn parse() {}".to_string(),
99            media_type: "text/x-rust".to_string(),
100        });
101
102        let bytes = pkt.to_bytes().unwrap();
103        let restored = ContextPacket::from_bytes(&bytes).unwrap();
104        assert_eq!(pkt, restored);
105    }
106}