use polyc_proto::proto::polychrome::agent::v1::{
Message, content, function_result_content, tool_call_content, tool_result_content,
};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MessageContentFold {
pub content: MessageContent,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MessageContent {
Text(TextFact),
ToolCall(ToolCallFact),
ToolResult(ToolResultFact),
None,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextFact {
pub position: u64,
pub turn_id: Option<String>,
pub text: String,
pub trust: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolCallFact {
pub position: u64,
pub turn_id: Option<String>,
pub tool_call_id: String,
pub name: String,
pub arguments: Value,
pub trust: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolResultFact {
pub position: u64,
pub turn_id: Option<String>,
pub tool_call_id: String,
pub name: String,
pub result: Value,
pub first_party: bool,
pub trust: String,
}
#[must_use]
pub fn fold_message_content(
message: &Message,
position: u64,
turn_id: Option<&str>,
trust: &str,
) -> MessageContentFold {
let mut warnings = Vec::new();
let Some(block) = message.content.as_option() else {
return MessageContentFold {
content: MessageContent::None,
warnings,
};
};
let content = match &block.r#type {
Some(content::Type::Text(t)) => MessageContent::Text(TextFact {
position,
turn_id: turn_id.map(str::to_owned),
text: t.text.clone(),
trust: trust.to_owned(),
}),
Some(content::Type::ToolCall(tc)) => {
let (name, arguments) = match &tc.r#type {
Some(tool_call_content::Type::FunctionCall(fc)) => {
let arguments = fc
.arguments
.as_option()
.map_or(Value::Null, struct_to_value);
(fc.name.clone(), arguments)
}
_ => (String::new(), Value::Null),
};
if turn_id.is_none() {
warnings.push(format!(
"pos {position}: tool_call with no turn suffix (orphaned, not attached to any turn)"
));
}
MessageContent::ToolCall(ToolCallFact {
position,
turn_id: turn_id.map(str::to_owned),
tool_call_id: tc.id.clone(),
name,
arguments,
trust: trust.to_owned(),
})
}
Some(content::Type::ToolResult(tr)) => {
let (name, result) = match &tr.r#type {
Some(tool_result_content::Type::FunctionResult(fr)) => {
let result = match &fr.result {
Some(function_result_content::Result::Response(s)) => struct_to_value(s),
_ => Value::Null,
};
(fr.name.clone(), result)
}
_ => (String::new(), Value::Null),
};
if turn_id.is_none() {
warnings.push(format!(
"pos {position}: tool_result with no turn suffix (orphaned, not attached to any turn)"
));
}
MessageContent::ToolResult(ToolResultFact {
position,
turn_id: turn_id.map(str::to_owned),
tool_call_id: tr.call_id.clone(),
name,
result,
first_party: tr.first_party,
trust: trust.to_owned(),
})
}
_ => MessageContent::None,
};
MessageContentFold { content, warnings }
}
fn struct_to_value(s: &impl serde::Serialize) -> Value {
serde_json::to_value(s).unwrap_or(Value::Null)
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs, clippy::unwrap_used)]
use buffa_types::google::protobuf::Struct;
use polyc_proto::proto::polychrome::agent::v1::{
Content, FunctionCallContent, FunctionResultContent, TextContent, ToolCallContent,
ToolResultContent,
};
use serde_json::json;
use super::*;
fn text_message(text: &str) -> Message {
Message {
role: "model".into(),
content: buffa::MessageField::some(Content {
r#type: Some(content::Type::Text(Box::new(TextContent {
text: text.to_owned(),
..Default::default()
}))),
..Default::default()
}),
..Default::default()
}
}
fn tool_call_message_fixture(id: &str, name: &str, args_json: &str) -> Message {
let arguments = serde_json::from_str::<Struct>(args_json)
.map(buffa::MessageField::some)
.unwrap_or_default();
Message {
role: "model".into(),
content: buffa::MessageField::some(Content {
r#type: Some(content::Type::ToolCall(Box::new(ToolCallContent {
id: id.to_owned(),
r#type: Some(tool_call_content::Type::FunctionCall(Box::new(
FunctionCallContent {
name: name.to_owned(),
arguments,
..Default::default()
},
))),
..Default::default()
}))),
..Default::default()
}),
..Default::default()
}
}
fn tool_result_message_fixture(id: &str, result_json: &str) -> Message {
let response = serde_json::from_str::<Struct>(result_json)
.ok()
.map(|s| function_result_content::Result::Response(Box::new(s)));
Message {
role: "tool".into(),
content: buffa::MessageField::some(Content {
r#type: Some(content::Type::ToolResult(Box::new(ToolResultContent {
call_id: id.to_owned(),
first_party: true,
r#type: Some(tool_result_content::Type::FunctionResult(Box::new(
FunctionResultContent {
result: response,
..Default::default()
},
))),
..Default::default()
}))),
..Default::default()
}),
..Default::default()
}
}
#[test]
fn text_block_folds_to_text_fact() {
let msg = text_message("hello");
let folded = fold_message_content(&msg, 1, Some("turn-1"), "trusted_user");
assert!(folded.warnings.is_empty());
match folded.content {
MessageContent::Text(t) => {
assert_eq!(t.position, 1);
assert_eq!(t.turn_id.as_deref(), Some("turn-1"));
assert_eq!(t.text, "hello");
assert_eq!(t.trust, "trusted_user");
}
other => panic!("expected Text, got {other:?}"),
}
}
#[test]
fn tool_call_folds_full_untruncated_arguments() {
let msg = tool_call_message_fixture("call-1", "search", r#"{"q":"a"}"#);
let folded = fold_message_content(&msg, 7, Some("turn-1"), "trusted_user");
assert!(folded.warnings.is_empty());
match folded.content {
MessageContent::ToolCall(c) => {
assert_eq!(c.position, 7);
assert_eq!(c.turn_id.as_deref(), Some("turn-1"));
assert_eq!(c.tool_call_id, "call-1");
assert_eq!(c.name, "search");
assert_eq!(c.arguments, json!({"q": "a"}));
assert_eq!(c.trust, "trusted_user");
}
other => panic!("expected ToolCall, got {other:?}"),
}
}
#[test]
fn tool_call_with_no_turn_warns_orphaned() {
let msg = tool_call_message_fixture("call-1", "search", r#"{}"#);
let folded = fold_message_content(&msg, 3, None, "trusted_user");
assert_eq!(
folded.warnings,
vec!["pos 3: tool_call with no turn suffix (orphaned, not attached to any turn)"]
);
}
#[test]
fn tool_result_folds_full_untruncated_result_and_first_party() {
let msg = tool_result_message_fixture("call-1", r#"{"hits":1}"#);
let folded = fold_message_content(&msg, 9, Some("turn-1"), "trusted_user");
assert!(folded.warnings.is_empty());
match folded.content {
MessageContent::ToolResult(r) => {
assert_eq!(r.position, 9);
assert_eq!(r.turn_id.as_deref(), Some("turn-1"));
assert_eq!(r.tool_call_id, "call-1");
assert_eq!(r.result, json!({"hits": 1.0}));
assert!(r.first_party);
assert_eq!(r.trust, "trusted_user");
}
other => panic!("expected ToolResult, got {other:?}"),
}
}
#[test]
fn tool_result_with_no_turn_warns_orphaned() {
let msg = tool_result_message_fixture("call-1", r#"{}"#);
let folded = fold_message_content(&msg, 4, None, "trusted_user");
assert_eq!(
folded.warnings,
vec!["pos 4: tool_result with no turn suffix (orphaned, not attached to any turn)"]
);
}
#[test]
fn empty_content_folds_to_none() {
let msg = Message {
role: "model".into(),
..Default::default()
};
let folded = fold_message_content(&msg, 0, Some("turn-1"), "trusted_user");
assert!(folded.warnings.is_empty());
assert_eq!(folded.content, MessageContent::None);
}
}