use candid::Principal;
use encoding_rs::{Encoding, UTF_8};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::{Map, json};
use std::{
borrow::Cow,
collections::{BTreeMap, HashMap},
str::FromStr,
};
use crate::{Json, json::normalize_strict_schema};
pub use ic_auth_types::{ByteArrayB64, ByteBufB64, Xid};
mod completion;
mod resource;
pub use completion::*;
pub use resource::*;
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct AgentInput {
pub name: String,
pub prompt: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub resources: Vec<Resource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub topics: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub meta: Option<RequestMeta>,
}
impl AgentInput {
pub fn new(name: String, prompt: String) -> Self {
Self {
name,
prompt,
resources: Vec::new(),
topics: None,
meta: None,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum PromptCommand {
#[default]
Ping,
Plain {
prompt: String,
},
Command {
command: String,
prompt: String,
},
}
impl From<String> for PromptCommand {
fn from(prompt: String) -> Self {
let trimmed = prompt.trim();
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("/ping") {
return Self::Ping;
}
let Some(stripped) = trimmed.strip_prefix('/') else {
return Self::Plain { prompt };
};
let command_end = stripped.find(char::is_whitespace).unwrap_or(stripped.len());
let command = &stripped[..command_end];
Self::Command {
command: command.to_lowercase(),
prompt,
}
}
}
impl PromptCommand {
pub fn command_argument(&self) -> Option<&str> {
let Self::Command { command, prompt } = self else {
return None;
};
let trimmed = prompt.trim();
let Some(stripped) = trimmed.strip_prefix('/') else {
return Some(trimmed);
};
let command_end = stripped.find(char::is_whitespace).unwrap_or(stripped.len());
if !stripped[..command_end].eq_ignore_ascii_case(command) {
return Some(trimmed);
}
Some(stripped[command_end..].trim())
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct AgentOutput {
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub thoughts: Option<String>,
pub usage: Usage,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub tools_usage: HashMap<String, Usage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub failed_reason: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<ToolCall>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub chat_history: Vec<Message>,
#[serde(skip)]
pub raw_history: Vec<Json>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub artifacts: Vec<Resource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct PartialAgentOutput {
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub thoughts: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub failed_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
}
impl AgentOutput {
pub fn into_tool_output(self) -> ToolOutput<Json> {
let AgentOutput {
content,
thoughts,
usage,
tools_usage,
failed_reason,
artifacts,
conversation,
session,
model,
..
} = self;
let has_metadata = thoughts.is_some()
|| failed_reason.is_some()
|| conversation.is_some()
|| session.is_some()
|| model.is_some();
let is_error = failed_reason
.as_ref()
.map(|reason| !reason.trim().is_empty());
let output = if has_metadata {
json!(PartialAgentOutput {
content,
thoughts,
failed_reason,
conversation,
session,
model,
})
} else {
serde_json::from_str::<Json>(&content).unwrap_or(Json::String(content))
};
ToolOutput {
output,
is_error,
artifacts,
usage,
tools_usage,
}
}
}
fn deserialize_content<'de, D>(deserializer: D) -> Result<Vec<ContentPart>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Json::deserialize(deserializer)?;
match value {
Json::Null => Ok(Vec::new()),
Json::String(s) => Ok(vec![ContentPart::Text { text: s }]),
Json::Array(_) => Vec::<ContentPart>::deserialize(value).map_err(serde::de::Error::custom),
_ => Err(serde::de::Error::custom(
"expected a string or array for content",
)),
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct Message {
pub role: String,
#[serde(default, deserialize_with = "deserialize_content")]
pub content: Vec<ContentPart>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<Principal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<u64>,
}
impl Message {
pub fn text(&self) -> Option<String> {
let mut texts: Vec<&str> = Vec::new();
for part in &self.content {
if let ContentPart::Text { text } = part {
texts.push(text);
}
}
if texts.is_empty() {
return None;
}
Some(texts.join("\n\n"))
}
pub fn thoughts(&self) -> Option<String> {
let mut thoughts: Vec<&str> = Vec::new();
for part in &self.content {
if let ContentPart::Reasoning { text } = part {
thoughts.push(text);
}
}
if thoughts.is_empty() {
return None;
}
Some(thoughts.join("\n\n"))
}
pub fn tool_calls(&self) -> Vec<ToolCall> {
let mut tool_calls: Vec<ToolCall> = Vec::new();
for part in &self.content {
if let ContentPart::ToolCall {
name,
args,
call_id,
} = part
{
tool_calls.push(ToolCall {
name: name.clone(),
args: args.clone(),
call_id: call_id.clone(),
result: None,
remote_id: None,
});
}
}
tool_calls
}
pub fn prune_content(&mut self) -> usize {
let original_len = self.content.len();
self.content.retain(|part| {
matches!(
part,
ContentPart::Text { .. }
| ContentPart::Reasoning { .. }
| ContentPart::Action { .. }
)
});
let pruned = original_len - self.content.len();
if pruned > 0 {
self.content.push(ContentPart::Text {
text: format!(
"[{} items (tool calls or files) pruned due to limits]",
pruned
),
});
}
pruned
}
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all_fields = "camelCase")]
pub enum ContentPart {
Text {
text: String,
},
Reasoning {
text: String,
},
FileData {
file_uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
mime_type: Option<String>,
},
InlineData {
mime_type: String,
data: ByteBufB64,
},
ToolCall {
name: String,
args: Json,
#[serde(skip_serializing_if = "Option::is_none")]
call_id: Option<String>,
},
ToolOutput {
name: String,
output: Json,
#[serde(skip_serializing_if = "Option::is_none")]
is_error: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
remote_id: Option<Principal>,
},
Action {
name: String,
payload: Json,
#[serde(skip_serializing_if = "Option::is_none")]
recipients: Option<Vec<Principal>>,
#[serde(skip_serializing_if = "Option::is_none")]
signature: Option<ByteBufB64>,
},
#[serde(untagged)]
Any(Json),
}
impl ContentPart {
pub fn any_from<T>(ty: &str, val: T) -> Self
where
T: Serialize,
{
let mut val = json!(val);
if let Some(map) = val.as_object_mut() {
map.insert("type".to_string(), ty.into());
}
ContentPart::Any(val)
}
pub fn any_into<T>(self, ty: &str) -> Result<T, Box<Self>>
where
T: DeserializeOwned,
{
if let ContentPart::Any(val) = &self
&& let Some(t) = val.get("type").and_then(|x| x.as_str())
&& t == ty
{
T::deserialize(val).map_err(|_| Box::new(self))
} else {
Err(Box::new(self))
}
}
}
pub fn part_to_data_url(data: &ByteBufB64, mime_type: Option<&String>) -> String {
format!(
"data:{};base64,{}",
mime_type.map(|m| m.as_str()).unwrap_or(""),
data.to_base64()
)
}
pub fn inline_data_from_data_url(data_url: &str) -> Option<(ByteBufB64, String)> {
if let Some(stripped) = data_url.strip_prefix("data:") {
let (meta, data_part) = stripped.split_once(",")?;
if let Some(mime_part) = meta.strip_suffix(";base64") {
if let Ok(data) = ByteBufB64::from_str(data_part) {
let mime_type = if mime_part.is_empty() {
infer2::get(&data)
.map(|t| t.mime_type().to_string())
.unwrap_or_else(|| "application/octet-stream".to_string())
} else {
mime_part.to_string()
};
Some((data, mime_type))
} else {
None
}
} else {
let data = decode_percent_encoded_bytes(data_part)?;
let mime_type = if meta.is_empty() {
infer2::get(&data)
.map(|t| t.mime_type().to_string())
.unwrap_or_else(|| "text/plain".to_string())
} else {
meta.to_string()
};
Some((data, mime_type))
}
} else if let Ok(data) = ByteBufB64::from_str(data_url) {
let mime_type = infer2::get(&data).map(|t| t.mime_type().to_string());
Some((
data,
mime_type.unwrap_or_else(|| "application/octet-stream".to_string()),
))
} else {
None
}
}
impl<'de> Deserialize<'de> for ContentPart {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Json::deserialize(deserializer)?;
match &value {
Json::String(s) => Ok(ContentPart::Text { text: s.clone() }),
Json::Object(map)
if matches!(
map.get("type").and_then(|t| t.as_str()),
Some(
"Text"
| "Reasoning"
| "FileData"
| "InlineData"
| "ToolCall"
| "ToolOutput"
| "Action"
)
) =>
{
#[derive(Deserialize)]
#[serde(tag = "type", rename_all_fields = "camelCase")]
enum Helper {
Text {
text: String,
},
Reasoning {
text: String,
},
FileData {
file_uri: String,
mime_type: Option<String>,
},
InlineData {
mime_type: String,
data: ByteBufB64,
},
ToolCall {
name: String,
args: Json,
call_id: Option<String>,
},
ToolOutput {
name: String,
output: Json,
is_error: Option<bool>,
call_id: Option<String>,
remote_id: Option<Principal>,
},
Action {
name: String,
payload: Json,
recipients: Option<Vec<Principal>>,
signature: Option<ByteBufB64>,
},
}
match serde_json::from_value::<Helper>(value) {
Ok(h) => Ok(match h {
Helper::Text { text } => ContentPart::Text { text },
Helper::Reasoning { text } => ContentPart::Reasoning { text },
Helper::FileData {
file_uri,
mime_type,
} => ContentPart::FileData {
file_uri,
mime_type,
},
Helper::InlineData { mime_type, data } => {
ContentPart::InlineData { mime_type, data }
}
Helper::ToolCall {
name,
args,
call_id,
} => ContentPart::ToolCall {
name,
args,
call_id,
},
Helper::ToolOutput {
name,
output,
is_error,
call_id,
remote_id,
} => ContentPart::ToolOutput {
name,
output,
is_error,
call_id,
remote_id,
},
Helper::Action {
name,
payload,
recipients,
signature,
} => ContentPart::Action {
name,
payload,
recipients,
signature,
},
}),
Err(err) => Err(serde::de::Error::custom(format!(
"invalid ContentPart: {err}"
))),
}
}
_ => Ok(ContentPart::Any(value)),
}
}
}
impl From<String> for ContentPart {
fn from(text: String) -> Self {
Self::Text { text }
}
}
impl From<Json> for ContentPart {
fn from(val: Json) -> Self {
if let Json::Object(map) = &val
&& let Some(t) = map.get("type").and_then(|x| x.as_str())
{
match t {
"Text" | "Reasoning" | "FileData" | "InlineData" | "ToolCall" | "ToolOutput"
| "Action" | "Any" => {
if let Ok(part) = serde_json::from_value::<ContentPart>(val.clone()) {
return part;
}
}
_ => {}
}
}
ContentPart::Any(val)
}
}
impl TryFrom<Resource> for ContentPart {
type Error = Resource;
fn try_from(res: Resource) -> Result<Self, Self::Error> {
if res.blob.as_ref().map(|v| !v.0.is_empty()).unwrap_or(false)
&& let Some(data) = res.blob
{
match resource_text_from_bytes(&data.0, res.mime_type.as_deref()) {
Some(text) => Ok(ContentPart::Text {
text: text.into_owned(),
}),
None => {
let data: ByteBufB64 = data.0.into();
let mime_type = res.mime_type.unwrap_or_else(|| {
infer2::get(&data)
.map(|t| t.mime_type())
.unwrap_or("application/octet-stream")
.to_string()
});
Ok(ContentPart::InlineData { mime_type, data })
}
}
} else if res
.uri
.as_ref()
.map(|v| !v.trim().is_empty())
.unwrap_or(false)
&& let Some(file_uri) = res.uri
{
Ok(ContentPart::FileData {
file_uri,
mime_type: res.mime_type,
})
} else {
Err(res)
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ToolInput<T> {
pub name: String,
pub args: T,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub resources: Vec<Resource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub meta: Option<RequestMeta>,
}
impl<T> ToolInput<T> {
pub fn new(name: String, args: T) -> Self {
Self {
name,
args,
resources: Vec::new(),
meta: None,
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ToolOutput<T> {
pub output: T,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub artifacts: Vec<Resource>,
pub usage: Usage,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub tools_usage: HashMap<String, Usage>,
}
impl<T> ToolOutput<T> {
pub fn new(output: T) -> Self {
Self {
output,
is_error: None,
artifacts: Vec::new(),
usage: Usage::default(),
tools_usage: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct RequestMeta {
#[serde(skip_serializing_if = "Option::is_none")]
pub engine: Option<Principal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(flatten)]
#[serde(skip_serializing_if = "Map::is_empty")]
pub extra: Map<String, Json>,
}
impl RequestMeta {
pub fn get_extra_as<T>(&self, key: &str) -> Option<T>
where
T: DeserializeOwned,
{
self.extra
.get(key)
.and_then(|value| serde_json::from_value(value.clone()).ok())
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Usage {
pub input_tokens: u64,
pub output_tokens: u64,
#[serde(default)]
pub cached_tokens: u64,
pub requests: u64,
}
impl Usage {
pub fn accumulate(&mut self, other: &Usage) {
self.input_tokens = self.input_tokens.saturating_add(other.input_tokens);
self.output_tokens = self.output_tokens.saturating_add(other.output_tokens);
self.cached_tokens = self.cached_tokens.saturating_add(other.cached_tokens);
self.requests = self.requests.saturating_add(other.requests);
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ToolCall {
pub name: String,
pub args: Json,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<ToolOutput<Json>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub remote_id: Option<Principal>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Function {
pub definition: FunctionDefinition,
pub supported_resource_tags: Vec<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct FunctionDefinition {
pub name: String,
pub description: String,
pub parameters: Json,
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}
impl FunctionDefinition {
pub fn name_with_prefix(mut self, prefix: &str) -> Self {
self.name = format!("{}{}", prefix, self.name);
self
}
pub fn normalize_strict_parameters(mut self) -> Self {
if self.strict.unwrap_or_default() {
self.parameters = normalize_strict_schema(self.parameters);
}
self
}
}
pub fn estimate_tokens(content: &str) -> usize {
content.len() / 3
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub struct Document {
pub metadata: BTreeMap<String, Json>,
pub content: Json,
}
impl Document {
pub fn from_text(id: &str, text: &str) -> Self {
Self {
metadata: BTreeMap::from([
("id".to_string(), id.into()),
("type".to_string(), "Text".into()),
]),
content: text.into(),
}
}
}
impl From<&Resource> for Document {
fn from(res: &Resource) -> Self {
let mut metadata = BTreeMap::from([
("id".to_string(), res._id.into()),
("type".to_string(), "Resource".into()),
]);
let mut rr = ResourceRef::from(res);
rr.blob = None; if let Json::Object(val) = json!(rr) {
metadata.extend(val);
};
let content = match res
.blob
.as_ref()
.and_then(|b| resource_text_from_bytes(&b.0, res.mime_type.as_deref()))
{
Some(text) => text.into_owned().into(),
None => Json::Null,
};
Self { metadata, content }
}
}
#[derive(Clone, Debug)]
pub struct Documents {
tag: String,
docs: Vec<Document>,
}
impl Default for Documents {
fn default() -> Self {
Self {
tag: "documents".to_string(),
docs: Vec::new(),
}
}
}
impl Documents {
pub fn new(tag: String, docs: Vec<Document>) -> Self {
Self { tag, docs }
}
pub fn with_tag(self, tag: String) -> Self {
Self { tag, ..self }
}
pub fn tag(&self) -> &str {
&self.tag
}
pub fn to_message(&self, rfc3339_datetime: &str) -> Option<Message> {
if self.docs.is_empty() {
return None;
}
Some(Message {
role: "user".into(),
content: vec![
format!("Current Datetime: {}\n\n---\n\n{}", rfc3339_datetime, self).into(),
],
name: Some("$system".into()),
..Default::default()
})
}
pub fn append(&mut self, doc: Document) {
self.docs.push(doc);
}
}
impl From<Vec<String>> for Documents {
fn from(texts: Vec<String>) -> Self {
let mut docs = Vec::new();
for (i, text) in texts.into_iter().enumerate() {
docs.push(Document {
content: text.into(),
metadata: BTreeMap::from([
("_id".to_string(), i.into()),
("type".to_string(), "Text".into()),
]),
});
}
Self {
docs,
..Default::default()
}
}
}
impl From<Vec<Document>> for Documents {
fn from(docs: Vec<Document>) -> Self {
Self {
docs,
..Default::default()
}
}
}
impl std::ops::Deref for Documents {
type Target = Vec<Document>;
fn deref(&self) -> &Self::Target {
&self.docs
}
}
impl std::ops::DerefMut for Documents {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.docs
}
}
impl AsRef<Vec<Document>> for Documents {
fn as_ref(&self) -> &Vec<Document> {
&self.docs
}
}
impl std::fmt::Display for Document {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
json!(self).fmt(f)
}
}
impl std::fmt::Display for Documents {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.docs.is_empty() {
return Ok(());
}
writeln!(f, "<{}>", self.tag)?;
for doc in &self.docs {
writeln!(f, "{}", doc)?;
}
write!(f, "</{}>", self.tag)
}
}
pub fn prompt_with_resources(prompt: String, resources: &mut Vec<Resource>) -> String {
let user_resources = text_resource_documents(resources);
if user_resources.is_empty() {
prompt
} else {
format!(
"{prompt}\n\n{}",
Documents::new("attachments".to_string(), user_resources)
)
}
}
pub fn text_resource_documents(resources: &mut Vec<Resource>) -> Vec<Document> {
let res = select_resources(resources, &["text".to_string(), "md".to_string()]);
let mut user_resources: Vec<Document> = Vec::with_capacity(res.len());
for resource in &res {
let doc = Document::from(resource);
if doc.content != Json::Null {
user_resources.push(doc);
}
}
user_resources
}
fn decode_percent_encoded_bytes(input: &str) -> Option<ByteBufB64> {
fn decode_hex(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
let bytes = input.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
match bytes[index] {
b'%' => {
let hi = *bytes.get(index + 1)?;
let lo = *bytes.get(index + 2)?;
decoded.push((decode_hex(hi)? << 4) | decode_hex(lo)?);
index += 3;
}
byte => {
decoded.push(byte);
index += 1;
}
}
}
Some(decoded.into())
}
pub fn utf8_text_from_bytes(data: &[u8]) -> Option<&str> {
let text = std::str::from_utf8(data).ok()?;
looks_like_text(text).then_some(text)
}
pub fn utf8_text_from(data: Vec<u8>) -> Option<String> {
let text = String::from_utf8(data).ok()?;
looks_like_text(&text).then_some(text)
}
pub fn text_from_bytes(data: &[u8]) -> Option<Cow<'_, str>> {
text_from_bytes_with_encoding(data, platform_text_encoding())
}
pub fn text_from(data: Vec<u8>) -> Option<String> {
text_from_bytes(&data).map(Cow::into_owned)
}
pub fn text_from_bytes_with_encoding<'a>(
data: &'a [u8],
fallback_encoding: Option<&'static Encoding>,
) -> Option<Cow<'a, str>> {
if let Some(text) = utf8_text_from_bytes(data) {
return Some(Cow::Borrowed(text));
}
let encoding = fallback_encoding?;
if encoding.name() == UTF_8.name() {
return None;
}
let (text, _, had_errors) = encoding.decode(data);
if had_errors || (!data.is_empty() && text.is_empty()) || !looks_like_text(&text) {
return None;
}
Some(Cow::Owned(text.into_owned()))
}
pub fn text_encoding_for_label(label: &str) -> Option<&'static Encoding> {
let label = label.trim();
if label.eq_ignore_ascii_case("utf8") {
return Some(UTF_8);
}
Encoding::for_label(label.as_bytes())
}
pub fn text_encoding_label(encoding: &'static Encoding) -> String {
if encoding.name() == UTF_8.name() {
"utf8".to_string()
} else {
encoding.name().to_ascii_lowercase()
}
}
pub fn platform_text_encoding() -> Option<&'static Encoding> {
#[cfg(target_os = "windows")]
{
windows_code_page_encoding(windows_file_code_page())
}
#[cfg(not(target_os = "windows"))]
{
None
}
}
pub fn windows_code_page_encoding(code_page: u32) -> Option<&'static Encoding> {
match code_page {
65001 => Some(UTF_8),
936 => Some(encoding_rs::GBK),
950 => Some(encoding_rs::BIG5),
932 => Some(encoding_rs::SHIFT_JIS),
949 => Some(encoding_rs::EUC_KR),
1250 => Some(encoding_rs::WINDOWS_1250),
1251 => Some(encoding_rs::WINDOWS_1251),
1252 => Some(encoding_rs::WINDOWS_1252),
1253 => Some(encoding_rs::WINDOWS_1253),
1254 => Some(encoding_rs::WINDOWS_1254),
1255 => Some(encoding_rs::WINDOWS_1255),
1256 => Some(encoding_rs::WINDOWS_1256),
1257 => Some(encoding_rs::WINDOWS_1257),
1258 => Some(encoding_rs::WINDOWS_1258),
_ => {
let windows_label = format!("windows-{code_page}");
Encoding::for_label(windows_label.as_bytes()).or_else(|| {
let cp_label = format!("cp{code_page}");
Encoding::for_label(cp_label.as_bytes())
})
}
}
}
fn resource_text_from_bytes<'a>(data: &'a [u8], mime_type: Option<&str>) -> Option<Cow<'a, str>> {
resource_text_from_bytes_with_encoding(data, mime_type, platform_text_encoding())
}
fn resource_text_from_bytes_with_encoding<'a>(
data: &'a [u8],
mime_type: Option<&str>,
fallback_encoding: Option<&'static Encoding>,
) -> Option<Cow<'a, str>> {
if let Some(text) = utf8_text_from_bytes(data) {
return Some(Cow::Borrowed(text));
}
if let Some(mime_type) = mime_type
&& !mime_type_allows_text_fallback(mime_type)
{
return None;
}
text_from_bytes_with_encoding(data, fallback_encoding)
}
fn mime_type_allows_text_fallback(mime_type: &str) -> bool {
let essence = mime_type
.split(';')
.next()
.unwrap_or(mime_type)
.trim()
.to_ascii_lowercase();
essence.is_empty()
|| essence.starts_with("text/")
|| essence.ends_with("+json")
|| essence.ends_with("+xml")
|| matches!(
essence.as_str(),
"application/json"
| "application/xml"
| "application/javascript"
| "application/x-javascript"
| "application/x-ndjson"
| "application/yaml"
| "application/x-yaml"
| "application/toml"
| "application/x-www-form-urlencoded"
)
}
#[cfg(target_os = "windows")]
fn windows_file_code_page() -> u32 {
unsafe { windows_sys::Win32::Globalization::GetACP() }
}
fn looks_like_text(text: &str) -> bool {
let mut sampled = 0usize;
let mut suspicious = 0usize;
for ch in text.chars().take(4096) {
sampled += 1;
if ch.is_control() && !matches!(ch, '\n' | '\r' | '\t') {
suspicious += 1;
}
}
sampled == 0 || suspicious * 100 / sampled <= 5
}
#[cfg(test)]
mod tests {
use super::*;
fn resource(id: u64, tags: &[&str]) -> Resource {
Resource {
_id: id,
name: format!("resource-{id}"),
tags: tags.iter().map(|tag| tag.to_string()).collect(),
..Default::default()
}
}
#[test]
fn test_agent_and_tool_constructors_default_optional_fields() {
let agent = AgentInput::new("planner".into(), "summarize this".into());
assert_eq!(agent.name, "planner");
assert_eq!(agent.prompt, "summarize this");
assert!(agent.resources.is_empty());
assert!(agent.topics.is_none());
assert!(agent.meta.is_none());
let tool = ToolInput::new("sum".into(), json!({"x": 1, "y": 2}));
assert_eq!(tool.name, "sum");
assert_eq!(tool.args, json!({"x": 1, "y": 2}));
assert!(tool.resources.is_empty());
assert!(tool.meta.is_none());
let output = ToolOutput::new(json!("ok"));
assert_eq!(output.output, json!("ok"));
assert!(output.artifacts.is_empty());
assert_eq!(output.usage.requests, 0);
assert!(output.tools_usage.is_empty());
}
#[test]
fn test_prompt_command_from_string_variants() {
assert_eq!(PromptCommand::from("".to_string()), PromptCommand::Ping);
assert_eq!(
PromptCommand::from(" /PING ".to_string()),
PromptCommand::Ping
);
assert_eq!(
PromptCommand::from("hello".to_string()),
PromptCommand::Plain {
prompt: "hello".into(),
}
);
assert_eq!(
PromptCommand::from("/Status show details".to_string()),
PromptCommand::Command {
command: "status".into(),
prompt: "/Status show details".into(),
}
);
assert_eq!(
PromptCommand::from("/help".to_string()),
PromptCommand::Command {
command: "help".into(),
prompt: "/help".into(),
}
);
let stop = PromptCommand::from("/stop 停止当前任务,保留会话".to_string());
assert_eq!(stop.command_argument(), Some("停止当前任务,保留会话"));
let manual = PromptCommand::Command {
command: "cancel".into(),
prompt: "取消当前任务".into(),
};
assert_eq!(manual.command_argument(), Some("取消当前任务"));
assert_eq!(PromptCommand::Ping.command_argument(), None);
}
#[test]
fn test_agent_output_into_tool_output_handles_json_plain_text_and_metadata() {
let mut tools_usage = HashMap::new();
tools_usage.insert(
"sum".into(),
Usage {
requests: 1,
..Default::default()
},
);
let output = AgentOutput {
content: r#"{"ok":true}"#.into(),
usage: Usage {
input_tokens: 2,
output_tokens: 1,
requests: 1,
..Default::default()
},
tools_usage: tools_usage.clone(),
artifacts: vec![resource(7, &["text"])],
..Default::default()
}
.into_tool_output();
assert_eq!(output.output, json!({"ok": true}));
assert_eq!(output.artifacts.len(), 1);
assert_eq!(output.artifacts[0]._id, 7);
assert_eq!(output.usage.input_tokens, 2);
assert_eq!(output.tools_usage.get("sum").unwrap().requests, 1);
let output = AgentOutput {
content: "not-json".into(),
thoughts: Some("thinking".into()),
session: Some("session-1".into()),
model: Some("test-model".into()),
..Default::default()
}
.into_tool_output();
assert_eq!(
output.output,
json!({
"content": "not-json",
"thoughts": "thinking",
"session": "session-1",
"model": "test-model"
})
);
let output = AgentOutput {
content: "still-not-json".into(),
..Default::default()
}
.into_tool_output();
assert_eq!(output.output, json!("still-not-json"));
}
#[test]
fn test_data_url_helpers_round_trip_and_invalid_inputs() {
let data: ByteBufB64 = b"hello".to_vec().into();
let mime_type = "text/plain".to_string();
let data_url = part_to_data_url(&data, Some(&mime_type));
assert_eq!(data_url, "data:text/plain;base64,aGVsbG8=");
let (decoded, decoded_mime_type) = inline_data_from_data_url(&data_url).unwrap();
assert_eq!(decoded, data);
assert_eq!(decoded_mime_type, "text/plain");
let (decoded, _) = inline_data_from_data_url("aGVsbG8=").unwrap();
assert_eq!(decoded, data);
let html_url = "data:text/html,%3Ch1%3EHello%2C%20World%21%3C%2Fh1%3E";
let (decoded, decoded_mime_type) = inline_data_from_data_url(html_url).unwrap();
let expected_html: ByteBufB64 = b"<h1>Hello, World!</h1>".to_vec().into();
assert_eq!(decoded, expected_html);
assert_eq!(decoded_mime_type, "text/html");
let (decoded, decoded_mime_type) =
inline_data_from_data_url("data:text/plain,hello").unwrap();
let expected_text: ByteBufB64 = b"hello".to_vec().into();
assert_eq!(decoded, expected_text);
assert_eq!(decoded_mime_type, "text/plain");
assert!(inline_data_from_data_url("data:text/plain,%GG").is_none());
assert!(inline_data_from_data_url("not-base64%%%").is_none());
}
#[test]
fn test_inline_data_from_data_url_infers_missing_mime_type() {
let jpeg_header: ByteBufB64 = vec![0xff, 0xd8, 0xff, 0xe0].into();
let data_url = format!("data:;base64,{}", jpeg_header.to_base64());
let (decoded, mime_type) = inline_data_from_data_url(&data_url).unwrap();
assert_eq!(decoded, jpeg_header);
assert_eq!(mime_type, "image/jpeg");
let (decoded, mime_type) = inline_data_from_data_url("data:;base64,aGVsbG8=").unwrap();
assert_eq!(decoded, ByteBufB64::from(b"hello".to_vec()));
assert_eq!(mime_type, "application/octet-stream");
let (decoded, mime_type) = inline_data_from_data_url("data:,Hello%20World").unwrap();
assert_eq!(decoded, ByteBufB64::from(b"Hello World".to_vec()));
assert_eq!(mime_type, "text/plain");
}
#[test]
fn test_content_part_try_from_resource_variants() {
let text = Resource {
blob: Some(b"hello".to_vec().into()),
..resource(1, &["text"])
};
assert_eq!(
ContentPart::try_from(text).unwrap(),
ContentPart::Text {
text: "hello".into(),
}
);
let binary = Resource {
blob: Some(vec![0xff, 0xd8, 0xff].into()),
mime_type: Some("image/jpeg".into()),
..resource(2, &["image"])
};
assert_eq!(
ContentPart::try_from(binary).unwrap(),
ContentPart::InlineData {
mime_type: "image/jpeg".into(),
data: vec![0xff, 0xd8, 0xff].into(),
}
);
let file = Resource {
uri: Some("file:///tmp/a.txt".into()),
mime_type: Some("text/plain".into()),
..resource(3, &["text"])
};
assert_eq!(
ContentPart::try_from(file).unwrap(),
ContentPart::FileData {
file_uri: "file:///tmp/a.txt".into(),
mime_type: Some("text/plain".into()),
}
);
let empty_blob = Resource {
blob: Some(Vec::<u8>::new().into()),
..resource(4, &["text"])
};
assert!(ContentPart::try_from(empty_blob).is_err());
let empty_uri = Resource {
uri: Some(" ".into()),
..resource(5, &["text"])
};
assert!(ContentPart::try_from(empty_uri).is_err());
}
#[test]
fn test_text_from_bytes_decodes_utf8_and_legacy_fallback() {
let decoded =
text_from_bytes_with_encoding("中文.txt".as_bytes(), Some(encoding_rs::GBK)).unwrap();
assert!(matches!(&decoded, Cow::Borrowed(_)));
assert_eq!(decoded.as_ref(), "中文.txt");
let (gbk, _, had_errors) = encoding_rs::GBK.encode("中文.txt");
assert!(!had_errors);
let decoded = text_from_bytes_with_encoding(&gbk, Some(encoding_rs::GBK)).unwrap();
assert!(matches!(&decoded, Cow::Owned(_)));
assert_eq!(decoded.as_ref(), "中文.txt");
assert!(text_from_bytes_with_encoding(&gbk, Some(UTF_8)).is_none());
assert!(text_from_bytes_with_encoding(&[0x81, 0x30], Some(encoding_rs::GBK)).is_none());
assert!(
resource_text_from_bytes_with_encoding(
&[0xff, 0xfe],
None,
Some(encoding_rs::WINDOWS_1252),
)
.is_none()
);
}
#[test]
fn test_resource_text_fallback_respects_binary_mime_type() {
let binary_header = [0xff, 0xd8, 0xff];
assert!(
resource_text_from_bytes_with_encoding(
&binary_header,
Some("image/jpeg"),
Some(encoding_rs::WINDOWS_1252),
)
.is_none()
);
let (legacy_text, _, had_errors) = encoding_rs::WINDOWS_1252.encode("café");
assert!(!had_errors);
let decoded = resource_text_from_bytes_with_encoding(
&legacy_text,
Some("text/plain; charset=windows-1252"),
Some(encoding_rs::WINDOWS_1252),
)
.unwrap();
assert_eq!(decoded.as_ref(), "café");
}
#[test]
fn test_request_meta_get_extra_as_and_usage_accumulate() {
let mut extra = Map::new();
extra.insert("numbers".into(), json!([1, 2, 3]));
extra.insert("flag".into(), json!(true));
let meta = RequestMeta {
extra,
..Default::default()
};
assert_eq!(
meta.get_extra_as::<Vec<u64>>("numbers"),
Some(vec![1, 2, 3])
);
assert_eq!(meta.get_extra_as::<bool>("flag"), Some(true));
assert_eq!(meta.get_extra_as::<String>("missing"), None);
let mut usage = Usage {
input_tokens: u64::MAX - 1,
output_tokens: 2,
cached_tokens: 3,
requests: u64::MAX,
};
let other = Usage {
input_tokens: 10,
output_tokens: 5,
cached_tokens: u64::MAX,
requests: 1,
};
usage.accumulate(&other);
assert_eq!(usage.input_tokens, u64::MAX);
assert_eq!(usage.output_tokens, 7);
assert_eq!(usage.cached_tokens, u64::MAX);
assert_eq!(usage.requests, u64::MAX);
}
#[test]
fn test_function_definition_and_document_helpers() {
let definition = FunctionDefinition {
name: "search".into(),
description: "Find documents".into(),
parameters: json!({
"type": "object",
"properties": {},
"required": [],
"additionalProperties": false
}),
strict: Some(true),
}
.name_with_prefix("tool_");
assert_eq!(definition.name, "tool_search");
assert_eq!(definition.description, "Find documents");
assert_eq!(estimate_tokens("abcdef"), 2);
assert_eq!(estimate_tokens(""), 0);
let text_doc = Document::from_text("doc-1", "hello");
assert_eq!(text_doc.metadata.get("id"), Some(&json!("doc-1")));
assert_eq!(text_doc.metadata.get("type"), Some(&json!("Text")));
assert_eq!(text_doc.content, json!("hello"));
let resource = Resource {
_id: 9,
name: "note".into(),
tags: vec!["text".into()],
uri: Some("file:///tmp/note.txt".into()),
blob: Some(b"hello".to_vec().into()),
mime_type: Some("text/plain".into()),
..Default::default()
};
let doc = Document::from(&resource);
assert_eq!(doc.metadata.get("id"), Some(&json!(9)));
assert_eq!(doc.metadata.get("type"), Some(&json!("Resource")));
assert_eq!(doc.metadata.get("_id"), Some(&json!(9)));
assert_eq!(doc.metadata.get("name"), Some(&json!("note")));
assert_eq!(doc.metadata.get("tags"), Some(&json!(["text"])));
assert_eq!(
doc.metadata.get("uri"),
Some(&json!("file:///tmp/note.txt"))
);
assert!(!doc.metadata.contains_key("blob"));
assert_eq!(doc.content, json!("hello"));
}
#[test]
fn test_documents_and_resource_prompt_helpers() {
let mut docs = Documents::new(
"attachments".into(),
vec![Document::from_text("1", "alpha")],
);
assert_eq!(docs.tag(), "attachments");
docs.append(Document::from_text("2", "beta"));
assert_eq!(docs.len(), 2);
let message = docs.to_message("2026-05-16T00:00:00Z").unwrap();
assert_eq!(message.role, "user");
assert_eq!(message.name.as_deref(), Some("$system"));
let text = message.text().unwrap();
assert!(text.contains("Current Datetime: 2026-05-16T00:00:00Z"));
assert!(text.contains("<attachments>"));
assert!(text.contains("alpha"));
assert!(text.contains("beta"));
assert!(
Documents::default()
.to_message("2026-05-16T00:00:00Z")
.is_none()
);
let from_strings: Documents = vec!["alpha".to_string(), "beta".to_string()].into();
assert_eq!(
from_strings[0],
Document {
metadata: BTreeMap::from([
("_id".to_string(), json!(0)),
("type".to_string(), json!("Text")),
]),
content: json!("alpha"),
}
);
assert_eq!(
from_strings[1],
Document {
metadata: BTreeMap::from([
("_id".to_string(), json!(1)),
("type".to_string(), json!("Text")),
]),
content: json!("beta"),
}
);
let mut resources = vec![
Resource {
blob: Some(b"alpha".to_vec().into()),
..resource(1, &["text"])
},
Resource {
blob: Some(vec![0xff, 0xfe].into()),
..resource(2, &["md"])
},
Resource {
uri: Some("file:///tmp/image.png".into()),
..resource(3, &["image"])
},
];
let docs = text_resource_documents(&mut resources);
assert_eq!(
docs,
vec![Document {
metadata: BTreeMap::from([
("_id".to_string(), json!(1)),
("id".to_string(), json!(1)),
("name".to_string(), json!("resource-1")),
("tags".to_string(), json!(["text"])),
("type".to_string(), json!("Resource")),
]),
content: json!("alpha"),
}]
);
assert_eq!(resources.len(), 1);
assert_eq!(resources[0]._id, 3);
let mut prompt_resources = vec![Resource {
blob: Some(b"beta".to_vec().into()),
..resource(4, &["text"])
}];
let prompt = prompt_with_resources("Base prompt".into(), &mut prompt_resources);
assert!(prompt.starts_with("Base prompt\n\n<attachments>"));
assert!(prompt.contains("beta"));
assert!(prompt_resources.is_empty());
let mut untouched_resources = vec![Resource {
uri: Some("file:///tmp/only-image.png".into()),
..resource(5, &["image"])
}];
let prompt = prompt_with_resources("Base prompt".into(), &mut untouched_resources);
assert_eq!(prompt, "Base prompt");
assert_eq!(untouched_resources.len(), 1);
assert_eq!(untouched_resources[0]._id, 5);
}
#[test]
fn test_message_content_deserialize_rejects_non_string_non_array() {
assert!(
serde_json::from_value::<Message>(json!({
"role": "user",
"content": 123,
}))
.is_err()
);
}
#[test]
fn test_prompt() {
let documents: Documents = vec![
Document {
metadata: BTreeMap::from([("_id".to_string(), 1.into())]),
content: "Test document 1.".into(),
},
Document {
metadata: BTreeMap::from([
("_id".to_string(), 2.into()),
("key".to_string(), "value".into()),
("a".to_string(), "b".into()),
]),
content: "Test document 2.".into(),
},
]
.into();
let s = documents.to_string();
let lines: Vec<&str> = s.lines().collect();
assert_eq!(lines[0], "<documents>");
assert_eq!(lines[3], "</documents>");
let doc1: Json = serde_json::from_str(lines[1]).unwrap();
assert_eq!(doc1.get("content").unwrap(), "Test document 1.");
assert_eq!(doc1.get("metadata").unwrap().get("_id").unwrap(), 1);
let doc2: Json = serde_json::from_str(lines[2]).unwrap();
assert_eq!(doc2.get("content").unwrap(), "Test document 2.");
assert_eq!(doc2.get("metadata").unwrap().get("_id").unwrap(), 2);
assert_eq!(doc2.get("metadata").unwrap().get("key").unwrap(), "value");
assert_eq!(doc2.get("metadata").unwrap().get("a").unwrap(), "b");
let documents = documents.with_tag("my_docs".to_string());
let s = documents.to_string();
let lines: Vec<&str> = s.lines().collect();
assert_eq!(lines[0], "<my_docs>");
assert_eq!(lines[3], "</my_docs>");
let doc1: Json = serde_json::from_str(lines[1]).unwrap();
assert_eq!(doc1.get("content").unwrap(), "Test document 1.");
assert_eq!(doc1.get("metadata").unwrap().get("_id").unwrap(), 1);
let doc2: Json = serde_json::from_str(lines[2]).unwrap();
assert_eq!(doc2.get("content").unwrap(), "Test document 2.");
assert_eq!(doc2.get("metadata").unwrap().get("_id").unwrap(), 2);
assert_eq!(doc2.get("metadata").unwrap().get("key").unwrap(), "value");
assert_eq!(doc2.get("metadata").unwrap().get("a").unwrap(), "b");
}
#[test]
fn test_content_part_text_serde_and_from() {
let part: ContentPart = "hello".to_string().into();
assert_eq!(
part,
ContentPart::Text {
text: "hello".into()
}
);
let v = serde_json::to_value(&part).unwrap();
assert_eq!(v.get("type").unwrap(), "Text");
assert_eq!(v.get("text").unwrap(), "hello");
let back: ContentPart = serde_json::from_value(v.clone()).unwrap();
assert_eq!(back, part);
let back: ContentPart = v.into();
assert_eq!(back, part);
let part: Vec<ContentPart> = serde_json::from_str(
r#"
[
"hello",
{
"type": "Text",
"text": "world"
}
]
"#,
)
.unwrap();
assert_eq!(
part,
vec![
ContentPart::Text {
text: "hello".into()
},
ContentPart::Text {
text: "world".into()
}
]
);
}
#[test]
fn test_content_part_filedata_serde_optional() {
let part = ContentPart::FileData {
file_uri: "gs://bucket/file".into(),
mime_type: None,
};
let v = serde_json::to_value(&part).unwrap();
assert_eq!(v.get("type").unwrap(), "FileData");
assert_eq!(v.get("fileUri").unwrap(), "gs://bucket/file");
assert!(v.get("mimeType").is_none());
let part2 = ContentPart::FileData {
file_uri: "gs://bucket/file2".into(),
mime_type: Some("image/png".into()),
};
let v2 = serde_json::to_value(&part2).unwrap();
assert_eq!(v2.get("type").unwrap(), "FileData");
assert_eq!(v2.get("fileUri").unwrap(), "gs://bucket/file2");
assert_eq!(v2.get("mimeType").unwrap(), "image/png");
let back: ContentPart = serde_json::from_value(v2.clone()).unwrap();
assert_eq!(back, part2);
let back: ContentPart = v2.into();
assert_eq!(back, part2);
}
#[test]
fn test_content_part_inlinedata_serde() {
let part = ContentPart::InlineData {
mime_type: "text/plain".into(),
data: b"hello".to_vec().into(),
};
let v = serde_json::to_value(&part).unwrap();
assert_eq!(v.get("type").unwrap(), "InlineData");
assert_eq!(v.get("mimeType").unwrap(), "text/plain");
assert_eq!(v.get("data").unwrap(), "aGVsbG8=");
let back: ContentPart = serde_json::from_value(v.clone()).unwrap();
assert_eq!(back, part);
let back: ContentPart = v.into();
assert_eq!(back, part);
}
#[test]
fn test_content_part_any_serde() {
let v = json!({
"type": "text/plain",
"data": "aGVsbG8=",
});
let part: ContentPart = v.clone().into();
assert_eq!(part, ContentPart::Any(v));
let v2 = serde_json::to_value(&part).unwrap();
assert_eq!(v2.get("type").unwrap(), "text/plain");
assert_eq!(v2.get("data").unwrap(), "aGVsbG8=");
let part = ContentPart::Any(json!({
"data": "aGVsbG8=",
}));
let v2 = serde_json::to_value(&part).unwrap();
assert!(v2.get("type").is_none());
assert_eq!(v2.get("data").unwrap(), "aGVsbG8=");
}
#[test]
fn test_content_part_any_supports_resource_serde() {
let mut metadata = Map::new();
metadata.insert("source".into(), json!("upload"));
metadata.insert("priority".into(), json!(3));
let resource = Resource {
_id: 42,
name: "note.txt".into(),
tags: vec!["text".into(), "note".into()],
description: Some("A note resource".into()),
uri: Some("file:///tmp/note.txt".into()),
mime_type: Some("text/plain".into()),
blob: Some(b"hello world".to_vec().into()),
size: Some(11),
metadata: Some(metadata),
..Default::default()
};
let resource_json = json!(resource);
let part: ContentPart = resource_json.clone().into();
assert_eq!(part, ContentPart::Any(resource_json.clone()));
let serialized = serde_json::to_value(&part).unwrap();
assert_eq!(serialized, resource_json);
let back: ContentPart = serde_json::from_value(serialized.clone()).unwrap();
assert_eq!(back, ContentPart::Any(resource_json));
let resource_back: Resource = serde_json::from_value(serialized).unwrap();
assert_eq!(resource_back._id, 42);
assert_eq!(resource_back.name, "note.txt");
assert_eq!(resource_back.tags, vec!["text", "note"]);
assert_eq!(
resource_back.description.as_deref(),
Some("A note resource")
);
assert_eq!(resource_back.uri.as_deref(), Some("file:///tmp/note.txt"));
assert_eq!(resource_back.mime_type.as_deref(), Some("text/plain"));
assert_eq!(resource_back.blob, Some(b"hello world".to_vec().into()));
assert_eq!(resource_back.size, Some(11));
assert_eq!(
resource_back
.metadata
.as_ref()
.and_then(|meta| meta.get("source")),
Some(&json!("upload"))
);
assert_eq!(
resource_back
.metadata
.as_ref()
.and_then(|meta| meta.get("priority")),
Some(&json!(3))
);
}
#[test]
fn test_content_part_any_from_and_any_into_resource() {
let mut metadata = Map::new();
metadata.insert("source".into(), json!("upload"));
metadata.insert("priority".into(), json!(3));
let resource = Resource {
_id: 42,
name: "note.txt".into(),
tags: vec!["text".into(), "note".into()],
description: Some("A note resource".into()),
uri: Some("file:///tmp/note.txt".into()),
mime_type: Some("text/plain".into()),
blob: Some(b"hello world".to_vec().into()),
size: Some(11),
metadata: Some(metadata),
..Default::default()
};
let part = ContentPart::any_from("Resource", &resource);
let expected = json!({
"type": "Resource",
"_id": 42,
"name": "note.txt",
"tags": ["text", "note"],
"description": "A note resource",
"uri": "file:///tmp/note.txt",
"mime_type": "text/plain",
"blob": "aGVsbG8gd29ybGQ=",
"size": 11,
"metadata": {
"source": "upload",
"priority": 3
}
});
assert_eq!(part, ContentPart::Any(expected));
let resource_back = part.clone().any_into::<Resource>("Resource").unwrap();
assert_eq!(resource_back._id, resource._id);
assert_eq!(resource_back.name, resource.name);
assert_eq!(resource_back.tags, resource.tags);
assert_eq!(resource_back.description, resource.description);
assert_eq!(resource_back.uri, resource.uri);
assert_eq!(resource_back.mime_type, resource.mime_type);
assert_eq!(resource_back.blob, resource.blob);
assert_eq!(resource_back.size, resource.size);
assert_eq!(resource_back.metadata, resource.metadata);
assert_eq!(
part.clone().any_into::<Resource>("OtherType"),
Err(Box::new(part.clone()))
);
let invalid = ContentPart::any_from("Resource", "plain-text");
assert_eq!(
invalid.clone().any_into::<Resource>("Resource"),
Err(Box::new(invalid))
);
}
#[test]
fn test_content_part_toolcall_and_tooloutput_serde() {
let call = ContentPart::ToolCall {
name: "sum".into(),
args: serde_json::json!({"x":1, "y":2}),
call_id: None,
};
let v_call = serde_json::to_value(&call).unwrap();
assert_eq!(v_call.get("type").unwrap(), "ToolCall");
assert_eq!(v_call.get("name").unwrap(), "sum");
assert_eq!(
v_call.get("args").unwrap(),
&serde_json::json!({"x":1, "y":2})
);
assert!(v_call.get("callId").is_none());
let back_call: ContentPart = serde_json::from_value(v_call.clone()).unwrap();
assert_eq!(back_call, call);
let back: ContentPart = v_call.into();
assert_eq!(back, call);
let out = ContentPart::ToolOutput {
name: "sum".into(),
output: serde_json::json!({"result":3}),
is_error: None,
call_id: Some("c1".into()),
remote_id: None,
};
let v_out = serde_json::to_value(&out).unwrap();
assert_eq!(v_out.get("type").unwrap(), "ToolOutput");
assert_eq!(v_out.get("name").unwrap(), "sum");
assert_eq!(
v_out.get("output").unwrap(),
&serde_json::json!({"result":3})
);
assert_eq!(v_out.get("callId").unwrap(), "c1");
let back_out: ContentPart = serde_json::from_value(v_out.clone()).unwrap();
assert_eq!(back_out, out);
let back: ContentPart = v_out.into();
assert_eq!(back, out);
}
#[test]
fn test_message_text_collects_only_text_parts_in_order() {
let msg = Message {
role: "assistant".into(),
content: vec![
ContentPart::Reasoning {
text: "first thought".into(),
},
ContentPart::Text {
text: "first text".into(),
},
ContentPart::ToolCall {
name: "sum".into(),
args: serde_json::json!({"x":1, "y":2}),
call_id: Some("call_1".into()),
},
ContentPart::Text {
text: "second text".into(),
},
ContentPart::Action {
name: "notify".into(),
payload: serde_json::json!({"ok": true}),
recipients: None,
signature: None,
},
],
..Default::default()
};
assert_eq!(msg.text().as_deref(), Some("first text\n\nsecond text"));
let no_text = Message {
role: "assistant".into(),
content: vec![ContentPart::Reasoning {
text: "thought only".into(),
}],
..Default::default()
};
assert_eq!(no_text.text(), None);
}
#[test]
fn test_message_thoughts_collects_only_reasoning_parts_in_order() {
let msg = Message {
role: "assistant".into(),
content: vec![
ContentPart::Text {
text: "visible text".into(),
},
ContentPart::Reasoning {
text: "first thought".into(),
},
ContentPart::ToolOutput {
name: "sum".into(),
output: serde_json::json!({"result": 3}),
is_error: None,
call_id: Some("call_1".into()),
remote_id: None,
},
ContentPart::Reasoning {
text: "second thought".into(),
},
],
..Default::default()
};
assert_eq!(
msg.thoughts().as_deref(),
Some("first thought\n\nsecond thought")
);
let no_reasoning = Message {
role: "assistant".into(),
content: vec![ContentPart::Text {
text: "text only".into(),
}],
..Default::default()
};
assert_eq!(no_reasoning.thoughts(), None);
}
#[test]
fn test_message_tool_calls_extract_from_content_parts() {
let parts = vec![
ContentPart::Text {
text: "hello".into(),
},
ContentPart::ToolCall {
name: "sum".into(),
args: serde_json::json!({"x":1, "y": 2}),
call_id: Some("abc".into()),
},
ContentPart::ToolCall {
name: "echo".into(),
args: serde_json::json!({"text":"hi"}),
call_id: None,
},
ContentPart::ToolOutput {
name: "sum".into(),
output: serde_json::json!({"result": 3}),
is_error: None,
call_id: Some("abc".into()),
remote_id: None,
},
];
let msg = Message {
role: "assistant".into(),
content: parts,
..Default::default()
};
let calls = msg.tool_calls();
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].name, "sum");
assert_eq!(calls[0].args, serde_json::json!({"x":1, "y":2}));
assert_eq!(calls[0].call_id.as_deref(), Some("abc"));
assert!(calls[0].result.is_none());
assert!(calls[0].remote_id.is_none());
assert_eq!(calls[1].name, "echo");
assert_eq!(calls[1].args, serde_json::json!({"text":"hi"}));
assert!(calls[1].call_id.is_none());
assert!(calls[1].result.is_none());
assert!(calls[1].remote_id.is_none());
}
#[test]
fn test_message_prune_content_keeps_visible_parts_and_is_idempotent() {
let action = ContentPart::Action {
name: "delegate".into(),
payload: serde_json::json!({"agent": "planner"}),
recipients: None,
signature: None,
};
let mut msg = Message {
role: "assistant".into(),
content: vec![
ContentPart::Text {
text: "visible text".into(),
},
ContentPart::ToolCall {
name: "sum".into(),
args: serde_json::json!({"x":1, "y":2}),
call_id: Some("call_1".into()),
},
ContentPart::Reasoning {
text: "visible thought".into(),
},
ContentPart::FileData {
file_uri: "file:///tmp/a.txt".into(),
mime_type: None,
},
action.clone(),
ContentPart::ToolOutput {
name: "sum".into(),
output: serde_json::json!({"result": 3}),
is_error: None,
call_id: Some("call_1".into()),
remote_id: None,
},
],
..Default::default()
};
assert_eq!(msg.prune_content(), 3);
assert_eq!(
msg.content,
vec![
ContentPart::Text {
text: "visible text".into(),
},
ContentPart::Reasoning {
text: "visible thought".into(),
},
action,
ContentPart::Text {
text: "[3 items (tool calls or files) pruned due to limits]".into(),
},
]
);
let pruned = msg.content.clone();
assert_eq!(msg.prune_content(), 0);
assert_eq!(msg.content, pruned);
}
#[test]
fn test_message_content_deserialize_from_string() {
let msg: Message = serde_json::from_value(serde_json::json!({
"role": "user",
"content": "hello world"
}))
.unwrap();
assert_eq!(msg.role, "user");
assert_eq!(msg.content.len(), 1);
assert_eq!(
msg.content[0],
ContentPart::Text {
text: "hello world".into()
}
);
let msg2: Message = serde_json::from_value(serde_json::json!({
"role": "assistant",
"content": [{"type": "Text", "text": "hi"}]
}))
.unwrap();
assert_eq!(msg2.content.len(), 1);
assert_eq!(msg2.content[0], ContentPart::Text { text: "hi".into() });
let msg3: Message = serde_json::from_value(serde_json::json!({
"role": "system"
}))
.unwrap();
assert!(msg3.content.is_empty());
let msg4: Message = serde_json::from_value(serde_json::json!({
"role": "assistant",
"content": null
}))
.unwrap();
assert!(msg4.content.is_empty());
}
#[test]
fn test_request_meta_extra_flatten_serde() {
let meta = RequestMeta {
engine: None,
user: None,
extra: Map::new(),
};
let v = serde_json::to_value(&meta).unwrap();
assert_eq!(v, serde_json::json!({}));
let mut extra = Map::new();
extra.insert("foo".into(), serde_json::json!("bar"));
extra.insert("n".into(), serde_json::json!(1));
extra.insert("obj".into(), serde_json::json!({"x": true}));
let meta2 = RequestMeta {
engine: Some(Principal::from_text("aaaaa-aa").unwrap()),
user: Some("alice".into()),
extra,
};
let v2 = serde_json::to_value(&meta2).unwrap();
assert_eq!(v2.get("engine").unwrap(), "aaaaa-aa");
assert_eq!(v2.get("user").unwrap(), "alice");
assert_eq!(v2.get("foo").unwrap(), "bar");
assert_eq!(v2.get("n").unwrap(), 1);
assert_eq!(v2.get("obj").unwrap(), &serde_json::json!({"x": true}));
assert!(v2.get("extra").is_none());
let input = serde_json::json!({
"engine": "aaaaa-aa",
"user": "bob",
"k1": "v1",
"k2": 2,
"nested": {"a": 1}
});
let back: RequestMeta = serde_json::from_value(input).unwrap();
assert_eq!(back.engine.unwrap().to_text(), "aaaaa-aa");
assert_eq!(back.user.as_deref(), Some("bob"));
assert_eq!(back.extra.get("k1").unwrap(), "v1");
assert_eq!(back.extra.get("k2").unwrap(), 2);
assert_eq!(
back.extra.get("nested").unwrap(),
&serde_json::json!({"a": 1})
);
let back2: RequestMeta = serde_json::from_value(v2).unwrap();
assert_eq!(back2.engine.unwrap().to_text(), "aaaaa-aa");
assert_eq!(back2.user.as_deref(), Some("alice"));
assert_eq!(back2.extra.get("foo").unwrap(), "bar");
assert_eq!(back2.extra.get("n").unwrap(), 1);
assert_eq!(
back2.extra.get("obj").unwrap(),
&serde_json::json!({"x": true})
);
}
}