use super::{AgentMessage, ModelConfig};
use crate::{core::model_resolver, utils::truncate::head_bytes};
use serde_json::{Value, json};
pub const COMPACTION_SUMMARY_PREFIX: &str = "The conversation history before this point was compacted into the following summary:\n\n<summary>\n";
pub const COMPACTION_SUMMARY_SUFFIX: &str = "\n</summary>";
pub const BRANCH_SUMMARY_PREFIX: &str =
"The following is a summary of a branch that this conversation came back from:\n\n<summary>\n";
pub const BRANCH_SUMMARY_SUFFIX: &str = "</summary>";
const ERROR_MAX_BYTES: usize = 1000;
pub(crate) fn truncate_for_error(s: &str) -> String {
if s.len() > ERROR_MAX_BYTES {
format!("{}...", head_bytes(s, ERROR_MAX_BYTES))
} else {
s.to_string()
}
}
pub(crate) fn sanitize_surrogates(s: &str) -> String {
s.to_string()
}
pub(crate) fn compat_flag(model: &ModelConfig, key: &str) -> bool {
model_resolver::provider_models(&model.provider)
.iter()
.find(|m| m.get("id").and_then(|v| v.as_str()) == Some(model.model_id.as_str()))
.and_then(|m| m.get("compat").and_then(|c| c.get(key)))
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
pub(crate) fn supports_mid_convo_system_messages(model: &ModelConfig) -> bool {
compat_flag(model, "supportsMidConvoSystemMessages")
}
pub(crate) fn collapse_mid_convo_system_messages(
messages: &[AgentMessage],
system_prompt: &str,
model: &ModelConfig,
) -> String {
if supports_mid_convo_system_messages(model) {
return system_prompt.to_string();
}
let extra: Vec<String> = messages
.iter()
.filter(|m| m.role == "system")
.map(|m| m.text())
.filter(|t| !t.trim().is_empty())
.collect();
if extra.is_empty() {
return system_prompt.to_string();
}
let mut parts: Vec<&str> = Vec::with_capacity(extra.len() + 1);
if !system_prompt.trim().is_empty() {
parts.push(system_prompt);
}
parts.extend(extra.iter().map(String::as_str));
parts.join("\n\n")
}
pub(crate) fn flush_pending_system_messages(params: &mut Vec<Value>, pending: &mut Vec<Value>) {
params.append(pending);
}
pub(crate) fn instruction_role(model: &ModelConfig) -> &'static str {
if model.reasoning && model.supports_developer_role {
"developer"
} else {
"system"
}
}
pub(crate) fn convert_system_message(msg: &AgentMessage, model: &ModelConfig) -> Option<Value> {
let text = msg.text();
if text.trim().is_empty() {
return None;
}
Some(json!({ "role": instruction_role(model), "content": sanitize_surrogates(&text) }))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn truncate_for_error_keeps_short_text_intact() {
assert_eq!(truncate_for_error("boom"), "boom");
let exact = "a".repeat(ERROR_MAX_BYTES);
assert_eq!(truncate_for_error(&exact), exact);
}
#[test]
fn truncate_for_error_does_not_split_multibyte_chars() {
let text = "中".repeat(ERROR_MAX_BYTES);
let out = truncate_for_error(&text);
assert!(out.ends_with("..."), "{out}");
assert!(out.chars().all(|c| c == '中' || c == '.'), "{out}");
assert!(out.len() <= ERROR_MAX_BYTES + 3);
assert!(truncate_for_error(&out).starts_with('中'));
}
}