bcp_types/
conversation.rs1use crate::enums::Role;
2use crate::error::TypeError;
3use crate::fields::{
4 decode_bytes_value, decode_field_header, decode_varint_value, encode_bytes_field,
5 encode_varint_field, skip_field,
6};
7
8#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct ConversationBlock {
30 pub role: Role,
31 pub content: Vec<u8>,
32 pub tool_call_id: Option<String>,
34}
35
36impl ConversationBlock {
37 pub fn encode_body(&self) -> Vec<u8> {
39 let mut buf = Vec::new();
40 encode_varint_field(&mut buf, 1, u64::from(self.role.to_wire_byte()));
41 encode_bytes_field(&mut buf, 2, &self.content);
42 if let Some(ref id) = self.tool_call_id {
43 encode_bytes_field(&mut buf, 3, id.as_bytes());
44 }
45 buf
46 }
47
48 pub fn decode_body(mut buf: &[u8]) -> Result<Self, TypeError> {
50 let mut role: Option<Role> = None;
51 let mut content: Option<Vec<u8>> = None;
52 let mut tool_call_id: Option<String> = None;
53
54 while !buf.is_empty() {
55 let (header, n) = decode_field_header(buf)?;
56 buf = &buf[n..];
57
58 match header.field_id {
59 1 => {
60 let (v, n) = decode_varint_value(buf)?;
61 buf = &buf[n..];
62 role = Some(Role::from_wire_byte(v as u8)?);
63 }
64 2 => {
65 let (data, n) = decode_bytes_value(buf)?;
66 buf = &buf[n..];
67 content = Some(data.to_vec());
68 }
69 3 => {
70 let (data, n) = decode_bytes_value(buf)?;
71 buf = &buf[n..];
72 tool_call_id = Some(String::from_utf8_lossy(data).into_owned());
73 }
74 _ => {
75 let n = skip_field(buf, header.wire_type)?;
76 buf = &buf[n..];
77 }
78 }
79 }
80
81 Ok(Self {
82 role: role.ok_or(TypeError::MissingRequiredField { field: "role" })?,
83 content: content.ok_or(TypeError::MissingRequiredField { field: "content" })?,
84 tool_call_id,
85 })
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn roundtrip_user_message() {
95 let block = ConversationBlock {
96 role: Role::User,
97 content: b"What is Rust?".to_vec(),
98 tool_call_id: None,
99 };
100 let body = block.encode_body();
101 let decoded = ConversationBlock::decode_body(&body).unwrap();
102 assert_eq!(decoded, block);
103 }
104
105 #[test]
106 fn roundtrip_tool_with_call_id() {
107 let block = ConversationBlock {
108 role: Role::Tool,
109 content: b"{ \"result\": 42 }".to_vec(),
110 tool_call_id: Some("call_abc123".to_string()),
111 };
112 let body = block.encode_body();
113 let decoded = ConversationBlock::decode_body(&body).unwrap();
114 assert_eq!(decoded, block);
115 }
116
117 #[test]
118 fn tool_call_id_absent_when_not_tool() {
119 let block = ConversationBlock {
120 role: Role::Assistant,
121 content: b"Here's the answer.".to_vec(),
122 tool_call_id: None,
123 };
124 let body = block.encode_body();
125 let decoded = ConversationBlock::decode_body(&body).unwrap();
126 assert_eq!(decoded.tool_call_id, None);
127 }
128}