Skip to main content

bcp_types/
tool_result.rs

1use crate::enums::Status;
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/// TOOL_RESULT block — represents output from a tool or MCP server.
9///
10/// Captures the name of the tool that was invoked, its execution status,
11/// the raw output, and an optional schema hint so downstream consumers
12/// know how to parse the content.
13///
14/// Field layout within body:
15///
16/// ```text
17/// ┌──────────┬───────────┬─────────────┬──────────────────────────┐
18/// │ Field ID │ Wire Type │ Name        │ Description              │
19/// ├──────────┼───────────┼─────────────┼──────────────────────────┤
20/// │ 1        │ Bytes     │ tool_name   │ Tool identifier          │
21/// │ 2        │ Varint    │ status      │ Status enum byte         │
22/// │ 3        │ Bytes     │ content     │ Tool output bytes        │
23/// │ 4        │ Bytes     │ schema_hint │ Schema hint (optional)   │
24/// └──────────┴───────────┴─────────────┴──────────────────────────┘
25/// ```
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct ToolResultBlock {
28    pub tool_name: String,
29    pub status: Status,
30    pub content: Vec<u8>,
31    /// Optional schema hint (e.g. "json-schema://...") to help
32    /// consumers parse the content field.
33    pub schema_hint: Option<String>,
34}
35
36impl ToolResultBlock {
37    /// Serialize this block's fields into a TLV-encoded body.
38    pub fn encode_body(&self) -> Vec<u8> {
39        let mut buf = Vec::new();
40        encode_bytes_field(&mut buf, 1, self.tool_name.as_bytes());
41        encode_varint_field(&mut buf, 2, u64::from(self.status.to_wire_byte()));
42        encode_bytes_field(&mut buf, 3, &self.content);
43        if let Some(ref hint) = self.schema_hint {
44            encode_bytes_field(&mut buf, 4, hint.as_bytes());
45        }
46        buf
47    }
48
49    /// Deserialize a TOOL_RESULT block from a TLV-encoded body.
50    pub fn decode_body(mut buf: &[u8]) -> Result<Self, TypeError> {
51        let mut tool_name: Option<String> = None;
52        let mut status: Option<Status> = None;
53        let mut content: Option<Vec<u8>> = None;
54        let mut schema_hint: Option<String> = None;
55
56        while !buf.is_empty() {
57            let (header, n) = decode_field_header(buf)?;
58            buf = &buf[n..];
59
60            match header.field_id {
61                1 => {
62                    let (data, n) = decode_bytes_value(buf)?;
63                    buf = &buf[n..];
64                    tool_name = Some(String::from_utf8_lossy(data).into_owned());
65                }
66                2 => {
67                    let (v, n) = decode_varint_value(buf)?;
68                    buf = &buf[n..];
69                    status = Some(Status::from_wire_byte(v as u8)?);
70                }
71                3 => {
72                    let (data, n) = decode_bytes_value(buf)?;
73                    buf = &buf[n..];
74                    content = Some(data.to_vec());
75                }
76                4 => {
77                    let (data, n) = decode_bytes_value(buf)?;
78                    buf = &buf[n..];
79                    schema_hint = Some(String::from_utf8_lossy(data).into_owned());
80                }
81                _ => {
82                    let n = skip_field(buf, header.wire_type)?;
83                    buf = &buf[n..];
84                }
85            }
86        }
87
88        Ok(Self {
89            tool_name: tool_name.ok_or(TypeError::MissingRequiredField { field: "tool_name" })?,
90            status: status.ok_or(TypeError::MissingRequiredField { field: "status" })?,
91            content: content.ok_or(TypeError::MissingRequiredField { field: "content" })?,
92            schema_hint,
93        })
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn roundtrip_ok_result() {
103        let block = ToolResultBlock {
104            tool_name: "read_file".to_string(),
105            status: Status::Ok,
106            content: b"file contents here".to_vec(),
107            schema_hint: None,
108        };
109        let body = block.encode_body();
110        let decoded = ToolResultBlock::decode_body(&body).unwrap();
111        assert_eq!(decoded, block);
112    }
113
114    #[test]
115    fn roundtrip_error_with_schema() {
116        let block = ToolResultBlock {
117            tool_name: "api_call".to_string(),
118            status: Status::Error,
119            content: b"404 Not Found".to_vec(),
120            schema_hint: Some("application/json".to_string()),
121        };
122        let body = block.encode_body();
123        let decoded = ToolResultBlock::decode_body(&body).unwrap();
124        assert_eq!(decoded, block);
125    }
126
127    #[test]
128    fn roundtrip_timeout() {
129        let block = ToolResultBlock {
130            tool_name: "slow_tool".to_string(),
131            status: Status::Timeout,
132            content: b"".to_vec(),
133            schema_hint: None,
134        };
135        let body = block.encode_body();
136        let decoded = ToolResultBlock::decode_body(&body).unwrap();
137        assert_eq!(decoded, block);
138    }
139}