use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::runtime::typed_id::{FileId, ImageId, MessageId, ModelId};
#[cfg(feature = "openapi")]
use utoipa::ToSchema;
use crate::execution_phase::{ExecutionPhase, PhaseSource};
use crate::message::ProviderOpaqueContent;
use crate::reasoning::ReasoningContentPart;
mod turn_scope;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum RuntimeMessageRole {
System,
User,
Agent,
ToolResult,
}
impl std::fmt::Display for RuntimeMessageRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RuntimeMessageRole::System => write!(f, "system"),
RuntimeMessageRole::User => write!(f, "user"),
RuntimeMessageRole::Agent => write!(f, "agent"),
RuntimeMessageRole::ToolResult => write!(f, "tool_result"),
}
}
}
impl From<&str> for RuntimeMessageRole {
fn from(s: &str) -> Self {
match s.to_lowercase().as_str() {
"system" => RuntimeMessageRole::System,
"user" => RuntimeMessageRole::User,
"agent" | "assistant" => RuntimeMessageRole::Agent,
"tool_result" => RuntimeMessageRole::ToolResult,
_ => RuntimeMessageRole::User,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ExternalActor {
pub actor_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub actor_name: Option<String>,
pub source: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<std::collections::HashMap<String, String>>,
}
impl ExternalActor {
pub fn display_label(&self) -> &str {
self.actor_name.as_deref().unwrap_or(&self.actor_id)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ReasoningConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<crate::model::ReasoningEffort>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct Controls {
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
pub model_id: Option<ModelId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub locale: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<ReasoningConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub speed: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verbosity: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_disclosure: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
pub hints: Option<std::collections::HashMap<String, serde_json::Value>>,
}
impl Controls {
pub fn resolve_hints(
session_hints: Option<&std::collections::HashMap<String, serde_json::Value>>,
message_hints: Option<&std::collections::HashMap<String, serde_json::Value>>,
) -> std::collections::HashMap<String, serde_json::Value> {
match (session_hints, message_hints) {
(None, None) => std::collections::HashMap::new(),
(Some(s), None) => s.clone(),
(None, Some(m)) => m.clone(),
(Some(s), Some(m)) => {
let mut merged = s.clone();
merged.extend(m.iter().map(|(k, v)| (k.clone(), v.clone())));
merged
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct RuntimeMessage {
#[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
pub id: MessageId,
pub role: RuntimeMessageRole,
pub content: Vec<ContentPart>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phase: Option<ExecutionPhase>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phase_source: Option<PhaseSource>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub controls: Option<Controls>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
pub metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub external_actor: Option<ExternalActor>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum ContentType {
Text,
Image,
ImageFile,
File,
ToolCall,
ToolResult,
Reasoning,
ProviderOpaque,
}
impl std::fmt::Display for ContentType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ContentType::Text => write!(f, "text"),
ContentType::Image => write!(f, "image"),
ContentType::ImageFile => write!(f, "image_file"),
ContentType::File => write!(f, "file"),
ContentType::ToolCall => write!(f, "tool_call"),
ContentType::ToolResult => write!(f, "tool_result"),
ContentType::Reasoning => write!(f, "reasoning"),
ContentType::ProviderOpaque => write!(f, "provider_opaque"),
}
}
}
impl From<&str> for ContentType {
fn from(s: &str) -> Self {
match s {
"image" => ContentType::Image,
"image_file" => ContentType::ImageFile,
"tool_call" => ContentType::ToolCall,
"tool_result" => ContentType::ToolResult,
"reasoning" => ContentType::Reasoning,
"provider_opaque" => ContentType::ProviderOpaque,
_ => ContentType::Text,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct TextContentPart {
pub text: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub annotations: Vec<TextAnnotation>,
}
impl TextContentPart {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
annotations: Vec::new(),
}
}
pub fn with_annotations(mut self, annotations: Vec<TextAnnotation>) -> Self {
self.annotations = annotations;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct TextAnnotation {
#[cfg_attr(feature = "openapi", schema(example = 0))]
pub start: usize,
#[cfg_attr(feature = "openapi", schema(example = 19))]
pub end: usize,
#[cfg_attr(feature = "openapi", schema(example = "citation_retrieval"))]
pub origin: String,
pub source: AnnotationSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "kchk_01j9y3q8w2"))]
pub external_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verified: Option<VerificationVerdict>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct AnnotationSource {
#[cfg_attr(
feature = "openapi",
schema(example = "github://owner/repo@main/docs/x.md")
)]
pub uri: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "Architecture Overview"))]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(
feature = "openapi",
schema(example = "The control plane owns durable state.")
)]
pub snippet: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub location: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct VerificationVerdict {
pub status: VerificationStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = 0.92))]
pub score: Option<f32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "openapi", schema(example = "entailed"))]
#[serde(rename_all = "snake_case")]
pub enum VerificationStatus {
Entailed,
Unsupported,
Uncertain,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ImageContentPart {
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base64: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
}
impl ImageContentPart {
pub fn from_url(url: impl Into<String>) -> Self {
Self {
url: Some(url.into()),
base64: None,
media_type: None,
}
}
pub fn from_base64(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
Self {
url: None,
base64: Some(base64.into()),
media_type: Some(media_type.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ImageFileContentPart {
#[cfg_attr(feature = "openapi", schema(value_type = String, example = "img_01933b5a00007000800000000000001"))]
pub image_id: ImageId,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
}
impl ImageFileContentPart {
pub fn new(image_id: ImageId) -> Self {
Self {
image_id,
filename: None,
}
}
pub fn with_filename(image_id: ImageId, filename: impl Into<String>) -> Self {
Self {
image_id,
filename: Some(filename.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct FileContentPart {
#[cfg_attr(feature = "openapi", schema(value_type = String, example = "file_01933b5a00007000800000000000001"))]
pub file_id: FileId,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
}
impl FileContentPart {
pub fn new(file_id: FileId) -> Self {
Self {
file_id,
filename: None,
}
}
pub fn with_filename(file_id: FileId, filename: impl Into<String>) -> Self {
Self {
file_id,
filename: Some(filename.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ToolCallContentPart {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub native: Option<crate::native_async::NativeToolCall>,
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
}
impl ToolCallContentPart {
pub fn from_native(
call: crate::native_async::NativeToolCall,
) -> crate::runtime::error::Result<Self> {
use crate::native_async::NativeToolCall;
call.validate()?;
let arguments = match &call {
NativeToolCall::Function { arguments, .. } => serde_json::from_str(arguments)
.map_err(|error| crate::runtime::error::AgentLoopError::llm(error.to_string()))?,
NativeToolCall::Custom { input, .. } => serde_json::Value::String(input.clone()),
};
Ok(Self {
id: call.id().into(),
name: call.name().into(),
arguments,
native: Some(call),
})
}
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
arguments: serde_json::Value,
) -> Self {
Self {
native: None,
id: id.into(),
name: name.into(),
arguments,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ToolResultContentPart {
pub tool_call_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl ToolResultContentPart {
pub fn new(
tool_call_id: impl Into<String>,
result: Option<serde_json::Value>,
error: Option<String>,
) -> Self {
Self {
tool_call_id: tool_call_id.into(),
result,
error,
}
}
pub fn success(tool_call_id: impl Into<String>, result: serde_json::Value) -> Self {
Self {
tool_call_id: tool_call_id.into(),
result: Some(result),
error: None,
}
}
pub fn error(tool_call_id: impl Into<String>, error: impl Into<String>) -> Self {
Self {
tool_call_id: tool_call_id.into(),
result: None,
error: Some(error.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ContentPart {
Text(TextContentPart),
Image(ImageContentPart),
ImageFile(ImageFileContentPart),
File(FileContentPart),
ToolCall(ToolCallContentPart),
ToolResult(ToolResultContentPart),
Reasoning(ReasoningContentPart),
ProviderOpaque(ProviderOpaqueContent),
}
impl ContentPart {
pub fn text(text: impl Into<String>) -> Self {
ContentPart::Text(TextContentPart::new(text))
}
pub fn tool_result_text(value: &serde_json::Value) -> Self {
match value {
serde_json::Value::String(text) => Self::text(text.clone()),
other => Self::text(other.to_string()),
}
}
pub fn image_url(url: impl Into<String>) -> Self {
ContentPart::Image(ImageContentPart::from_url(url))
}
pub fn image_file(image_id: ImageId) -> Self {
ContentPart::ImageFile(ImageFileContentPart::new(image_id))
}
pub fn file(file_id: FileId) -> Self {
ContentPart::File(FileContentPart::new(file_id))
}
pub fn tool_call(
id: impl Into<String>,
name: impl Into<String>,
arguments: serde_json::Value,
) -> Self {
ContentPart::ToolCall(ToolCallContentPart::new(id, name, arguments))
}
pub fn tool_result(
tool_call_id: impl Into<String>,
result: Option<serde_json::Value>,
error: Option<String>,
) -> Self {
ContentPart::ToolResult(ToolResultContentPart::new(tool_call_id, result, error))
}
pub fn reasoning(part: ReasoningContentPart) -> Self {
ContentPart::Reasoning(part)
}
pub fn as_reasoning(&self) -> Option<&ReasoningContentPart> {
match self {
ContentPart::Reasoning(r) => Some(r),
_ => None,
}
}
pub fn is_reasoning(&self) -> bool {
matches!(self, ContentPart::Reasoning(_))
}
pub fn as_text(&self) -> Option<&str> {
match self {
ContentPart::Text(t) => Some(&t.text),
_ => None,
}
}
pub fn is_image_file(&self) -> bool {
matches!(self, ContentPart::ImageFile(_))
}
pub fn is_file(&self) -> bool {
matches!(self, ContentPart::File(_))
}
pub fn content_type(&self) -> ContentType {
match self {
ContentPart::Text(_) => ContentType::Text,
ContentPart::Image(_) => ContentType::Image,
ContentPart::ImageFile(_) => ContentType::ImageFile,
ContentPart::File(_) => ContentType::File,
ContentPart::ToolCall(_) => ContentType::ToolCall,
ContentPart::ToolResult(_) => ContentType::ToolResult,
ContentPart::Reasoning(_) => ContentType::Reasoning,
ContentPart::ProviderOpaque(_) => ContentType::ProviderOpaque,
}
}
pub fn to_openai_format(&self) -> Option<serde_json::Value> {
match self {
ContentPart::Text(t) => Some(serde_json::json!({
"type": "text",
"text": t.text
})),
ContentPart::Image(img) => {
if let Some(url) = &img.url {
Some(serde_json::json!({
"type": "image_url",
"image_url": { "url": url }
}))
} else if let Some(b64) = &img.base64 {
let media_type = img.media_type.as_deref().unwrap_or("image/png");
Some(serde_json::json!({
"type": "image_url",
"image_url": { "url": format!("data:{};base64,{}", media_type, b64) }
}))
} else {
None
}
}
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InputContentPart {
Text(TextContentPart),
Image(ImageContentPart),
ImageFile(ImageFileContentPart),
File(FileContentPart),
}
impl From<InputContentPart> for ContentPart {
fn from(input: InputContentPart) -> Self {
match input {
InputContentPart::Text(t) => ContentPart::Text(t),
InputContentPart::Image(i) => ContentPart::Image(i),
InputContentPart::ImageFile(f) => ContentPart::ImageFile(f),
InputContentPart::File(f) => ContentPart::File(f),
}
}
}
impl InputContentPart {
pub fn text(text: impl Into<String>) -> Self {
InputContentPart::Text(TextContentPart::new(text))
}
pub fn image_url(url: impl Into<String>) -> Self {
InputContentPart::Image(ImageContentPart::from_url(url))
}
pub fn image_file(image_id: ImageId) -> Self {
InputContentPart::ImageFile(ImageFileContentPart::new(image_id))
}
pub fn file(file_id: FileId) -> Self {
InputContentPart::File(FileContentPart::new(file_id))
}
pub fn as_text(&self) -> Option<&str> {
match self {
InputContentPart::Text(t) => Some(&t.text),
_ => None,
}
}
pub fn content_type(&self) -> ContentType {
match self {
InputContentPart::Text(_) => ContentType::Text,
InputContentPart::Image(_) => ContentType::Image,
InputContentPart::ImageFile(_) => ContentType::ImageFile,
InputContentPart::File(_) => ContentType::File,
}
}
}
pub const MESSAGE_ORIGIN_METADATA_KEY: &str = "everruns_origin";
pub const MESSAGE_ORIGIN_TASK_WAKE: &str = "task_wake";
pub fn task_wake_message_metadata() -> std::collections::HashMap<String, serde_json::Value> {
std::collections::HashMap::from([(
MESSAGE_ORIGIN_METADATA_KEY.to_string(),
serde_json::Value::String(MESSAGE_ORIGIN_TASK_WAKE.to_string()),
)])
}
pub fn strip_reserved_message_metadata(
metadata: &mut Option<std::collections::HashMap<String, serde_json::Value>>,
) {
if let Some(map) = metadata.as_mut() {
map.remove(MESSAGE_ORIGIN_METADATA_KEY);
if map.is_empty() {
*metadata = None;
}
}
}
impl RuntimeMessage {
pub fn origin(&self) -> Option<&str> {
self.metadata
.as_ref()?
.get(MESSAGE_ORIGIN_METADATA_KEY)?
.as_str()
}
pub fn is_task_wake(&self) -> bool {
self.origin() == Some(MESSAGE_ORIGIN_TASK_WAKE)
}
pub fn reasoning_parts(&self) -> impl Iterator<Item = &ReasoningContentPart> {
self.content.iter().filter_map(ContentPart::as_reasoning)
}
pub fn has_reasoning(&self) -> bool {
self.content.iter().any(ContentPart::is_reasoning)
}
pub fn reasoning_display_text(&self) -> Option<String> {
let joined = self
.reasoning_parts()
.filter_map(ReasoningContentPart::display_text)
.collect::<Vec<_>>()
.join("\n\n");
(!joined.is_empty()).then_some(joined)
}
pub fn into_public(mut self) -> Self {
self.content
.retain(|part| !matches!(part, ContentPart::ProviderOpaque(_)));
for part in &mut self.content {
if let ContentPart::Reasoning(r) = part {
*r = r.to_public();
}
}
self
}
pub fn with_id(mut self, id: MessageId) -> Self {
self.id = id;
self
}
pub fn user(content: impl Into<String>) -> Self {
Self {
id: MessageId::new(),
role: RuntimeMessageRole::User,
content: vec![ContentPart::text(content)],
phase: None,
phase_source: None,
controls: None,
metadata: None,
external_actor: None,
created_at: Utc::now(),
}
}
pub fn assistant(content: impl Into<String>) -> Self {
Self {
id: MessageId::new(),
role: RuntimeMessageRole::Agent,
content: vec![ContentPart::text(content)],
phase: None,
phase_source: None,
controls: None,
metadata: None,
external_actor: None,
created_at: Utc::now(),
}
}
pub fn assistant_with_tools(
content: impl Into<String>,
tool_calls: Vec<crate::runtime::tool_types::ToolCall>,
) -> Self {
let text_content = content.into();
let mut parts = Vec::new();
if !text_content.is_empty() {
parts.push(ContentPart::text(text_content));
}
for tc in tool_calls {
parts.push(ContentPart::ToolCall(ToolCallContentPart {
native: None,
id: tc.id,
name: tc.name,
arguments: tc.arguments,
}));
}
Self {
id: MessageId::new(),
role: RuntimeMessageRole::Agent,
content: parts,
phase: None,
phase_source: None,
controls: None,
metadata: None,
external_actor: None,
created_at: Utc::now(),
}
}
pub fn system(content: impl Into<String>) -> Self {
Self {
id: MessageId::new(),
role: RuntimeMessageRole::System,
content: vec![ContentPart::text(content)],
phase: None,
phase_source: None,
controls: None,
metadata: None,
external_actor: None,
created_at: Utc::now(),
}
}
pub fn tool_result(
tool_call_id: impl Into<String>,
result: Option<serde_json::Value>,
error: Option<String>,
) -> Self {
let tool_call_id = tool_call_id.into();
Self {
id: MessageId::new(),
role: RuntimeMessageRole::ToolResult,
content: vec![ContentPart::ToolResult(ToolResultContentPart::new(
tool_call_id,
result,
error,
))],
phase: None,
phase_source: None,
controls: None,
metadata: None,
external_actor: None,
created_at: Utc::now(),
}
}
pub fn tool_result_with_images(
tool_call_id: impl Into<String>,
result: Option<serde_json::Value>,
images: Vec<crate::tool_types::ToolResultImage>,
) -> Self {
let tool_call_id = tool_call_id.into();
let mut content = vec![ContentPart::ToolResult(ToolResultContentPart::new(
tool_call_id,
result,
None,
))];
for img in images {
content.push(ContentPart::Image(ImageContentPart::from_base64(
img.base64,
img.media_type,
)));
}
Self {
id: MessageId::new(),
role: RuntimeMessageRole::ToolResult,
content,
phase: None,
phase_source: None,
controls: None,
metadata: None,
external_actor: None,
created_at: Utc::now(),
}
}
pub fn with_phase(mut self, phase: ExecutionPhase) -> Self {
self.phase = Some(phase);
self
}
pub fn with_phase_from(mut self, phase: ExecutionPhase, source: PhaseSource) -> Self {
self.phase = Some(phase);
self.phase_source = Some(source);
self
}
pub fn tool_call_id(&self) -> Option<&str> {
self.content.iter().find_map(|p| match p {
ContentPart::ToolResult(tr) => Some(tr.tool_call_id.as_str()),
_ => None,
})
}
pub fn text(&self) -> Option<&str> {
self.content.iter().find_map(|p| p.as_text())
}
pub fn tool_calls(&self) -> Vec<&ToolCallContentPart> {
self.content
.iter()
.filter_map(|p| match p {
ContentPart::ToolCall(tc) => Some(tc),
_ => None,
})
.collect()
}
pub fn has_tool_calls(&self) -> bool {
self.content
.iter()
.any(|p| matches!(p, ContentPart::ToolCall(_)))
}
pub fn tool_result_content(&self) -> Option<&ToolResultContentPart> {
self.content.iter().find_map(|p| match p {
ContentPart::ToolResult(tr) => Some(tr),
_ => None,
})
}
pub fn content_to_llm_string(&self) -> String {
self.content
.iter()
.map(|part| match part {
ContentPart::Text(t) => t.text.clone(),
ContentPart::Reasoning(_) => String::new(),
ContentPart::ProviderOpaque(_) => String::new(),
ContentPart::Image(_) => "[Image]".to_string(),
ContentPart::ImageFile(_) => "[Image File]".to_string(),
ContentPart::File(part) => part
.filename
.clone()
.map(|n| format!("[PDF File: {}]", n))
.unwrap_or_else(|| "[PDF File]".to_string()),
ContentPart::ToolCall(tc) => {
format!(
"Tool call: {} with arguments: {}",
tc.name,
serde_json::to_string(&tc.arguments).unwrap_or_default()
)
}
ContentPart::ToolResult(tr) => {
if let Some(err) = &tr.error {
format!("Tool error: {}", err)
} else if let Some(res) = &tr.result {
serde_json::to_string(res).unwrap_or_else(|_| "{}".to_string())
} else {
"{}".to_string()
}
}
})
.filter(|rendered| !rendered.is_empty())
.collect::<Vec<_>>()
.join("\n")
}
pub fn to_openai_format(&self) -> serde_json::Value {
let role = match self.role {
RuntimeMessageRole::System => "system",
RuntimeMessageRole::User => "user",
RuntimeMessageRole::Agent => "assistant",
RuntimeMessageRole::ToolResult => "tool",
};
if self.role == RuntimeMessageRole::ToolResult {
let tool_call_id = self.tool_call_id().unwrap_or("");
let content = self
.content
.iter()
.find_map(|p| match p {
ContentPart::ToolResult(tr) => {
if let Some(error) = &tr.error {
Some(format!("Error: {}", error))
} else if let Some(result) = &tr.result {
Some(serde_json::to_string(result).unwrap_or_else(|_| "{}".to_string()))
} else {
Some("{}".to_string())
}
}
_ => None,
})
.unwrap_or_else(|| "{}".to_string());
return serde_json::json!({
"role": role,
"content": content,
"tool_call_id": tool_call_id
});
}
if self.role == RuntimeMessageRole::Agent {
let tool_calls: Vec<serde_json::Value> = self
.content
.iter()
.filter_map(|p| match p {
ContentPart::ToolCall(tc) => Some(serde_json::json!({
"id": tc.id,
"type": "function",
"function": {
"name": tc.name,
"arguments": serde_json::to_string(&tc.arguments).unwrap_or_else(|_| "{}".to_string())
}
})),
_ => None,
})
.collect();
let text_content: String = self
.content
.iter()
.filter_map(|p| match p {
ContentPart::Text(t) => Some(t.text.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n");
if tool_calls.is_empty() {
return serde_json::json!({
"role": role,
"content": text_content
});
} else {
let mut result = serde_json::json!({
"role": role,
"tool_calls": tool_calls
});
if !text_content.is_empty() {
result["content"] = serde_json::json!(text_content);
}
return result;
}
}
let content = self.content_to_openai_format();
serde_json::json!({
"role": role,
"content": content
})
}
fn content_to_openai_format(&self) -> serde_json::Value {
if self.content.len() == 1
&& let ContentPart::Text(t) = &self.content[0]
{
return serde_json::json!(t.text);
}
let parts: Vec<serde_json::Value> = self
.content
.iter()
.filter_map(|part| part.to_openai_format())
.collect();
if parts.is_empty() {
return serde_json::json!("");
}
if parts.len() == 1
&& let Some(text) = parts[0].get("text")
{
return text.clone();
}
serde_json::json!(parts)
}
}
pub fn patch_dangling_tool_calls(messages: &[RuntimeMessage]) -> Vec<RuntimeMessage> {
let mut result = Vec::new();
for (i, msg) in messages.iter().enumerate() {
result.push(msg.clone());
if msg.role == RuntimeMessageRole::Agent && msg.has_tool_calls() {
for tc in msg.tool_calls() {
let has_result = messages[(i + 1)..].iter().any(|m| {
m.role == RuntimeMessageRole::ToolResult && m.tool_call_id() == Some(&tc.id)
});
if !has_result {
result.push(RuntimeMessage::tool_result(
&tc.id,
None,
Some(
"cancelled - another message came in before it could be completed"
.to_string(),
),
));
}
}
}
}
result
}
#[cfg(test)]
#[path = "message_tests.rs"]
mod tests;