use std::pin::Pin;
use async_trait::async_trait;
use crate::model::Prompty;
use crate::types::Message;
#[derive(Debug, thiserror::Error)]
pub enum InvokerError {
#[error("no {group} registered for key '{key}'")]
NotFound { group: String, key: String },
#[error("render error: {0}")]
Render(Box<dyn std::error::Error + Send + Sync>),
#[error("parse error: {0}")]
Parse(Box<dyn std::error::Error + Send + Sync>),
#[error("execute error: {0}")]
Execute(Box<dyn std::error::Error + Send + Sync>),
#[error("process error: {0}")]
Process(Box<dyn std::error::Error + Send + Sync>),
#[error("validation error: {0}")]
Validation(String),
#[error("load error: {0}")]
Load(String),
#[error("cancelled: {0}")]
Cancelled(String),
#[error("{0}")]
ExecuteRetryExhausted(ExecuteError),
#[error("{0}")]
Other(String),
}
#[derive(Debug)]
pub struct ExecuteError {
pub message: String,
pub messages: Vec<crate::types::Message>,
}
impl std::fmt::Display for ExecuteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ExecuteError {}
#[async_trait]
pub trait Renderer: Send + Sync {
async fn render(
&self,
agent: &Prompty,
template: &str,
inputs: &serde_json::Value,
) -> Result<String, InvokerError>;
}
#[async_trait]
pub trait Parser: Send + Sync {
fn pre_render(&self, _template: &str) -> Option<(String, serde_json::Value)> {
None
}
async fn parse(
&self,
agent: &Prompty,
rendered: &str,
context: Option<&serde_json::Value>,
) -> Result<Vec<Message>, InvokerError>;
}
#[async_trait]
pub trait Executor: Send + Sync {
async fn execute(
&self,
agent: &Prompty,
messages: &[Message],
) -> Result<serde_json::Value, InvokerError>;
async fn execute_stream(
&self,
_agent: &Prompty,
_messages: &[Message],
) -> Result<Pin<Box<dyn futures::Stream<Item = serde_json::Value> + Send>>, InvokerError> {
Err(InvokerError::Execute(
"Streaming not supported by this executor"
.to_string()
.into(),
))
}
fn format_tool_messages(
&self,
_raw_response: &serde_json::Value,
tool_calls: &[crate::types::ToolCall],
tool_results: &[String],
_text_content: Option<&str>,
) -> Vec<Message> {
let mut messages = Vec::new();
let mut assistant_meta = serde_json::Map::new();
let tc_value: Vec<serde_json::Value> = tool_calls
.iter()
.map(|tc| {
serde_json::json!({
"id": tc.id,
"type": "function",
"function": {
"name": tc.name,
"arguments": tc.arguments,
}
})
})
.collect();
assistant_meta.insert("tool_calls".into(), serde_json::Value::Array(tc_value));
messages.push(Message {
role: crate::types::Role::Assistant,
parts: vec![],
metadata: assistant_meta,
});
for (tc, result) in tool_calls.iter().zip(tool_results.iter()) {
messages.push(Message::tool_result(&tc.id, result));
}
messages
}
}
#[async_trait]
pub trait Processor: Send + Sync {
async fn process(
&self,
agent: &Prompty,
response: serde_json::Value,
) -> Result<serde_json::Value, InvokerError>;
fn process_stream(
&self,
_inner: Pin<Box<dyn futures::Stream<Item = serde_json::Value> + Send>>,
) -> Result<Pin<Box<dyn futures::Stream<Item = crate::types::StreamChunk> + Send>>, InvokerError>
{
Err(InvokerError::Process(
"Streaming not supported by this processor"
.to_string()
.into(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
struct DefaultFormatExecutor;
#[async_trait]
impl Executor for DefaultFormatExecutor {
async fn execute(
&self,
_agent: &Prompty,
_messages: &[Message],
) -> Result<serde_json::Value, InvokerError> {
Ok(serde_json::json!({}))
}
}
#[test]
fn test_default_format_tool_messages_single() {
let executor = DefaultFormatExecutor;
let tool_calls = vec![crate::types::ToolCall {
id: "call_1".into(),
name: "get_weather".into(),
arguments: r#"{"city":"NYC"}"#.into(),
}];
let results = vec!["72°F".to_string()];
let msgs =
executor.format_tool_messages(&serde_json::json!({}), &tool_calls, &results, None);
assert_eq!(msgs.len(), 2);
assert_eq!(msgs[0].role, crate::types::Role::Assistant);
let tc_meta = msgs[0]
.metadata
.get("tool_calls")
.unwrap()
.as_array()
.unwrap();
assert_eq!(tc_meta.len(), 1);
assert_eq!(tc_meta[0]["id"], "call_1");
assert_eq!(tc_meta[0]["type"], "function");
assert_eq!(tc_meta[0]["function"]["name"], "get_weather");
assert_eq!(tc_meta[0]["function"]["arguments"], r#"{"city":"NYC"}"#);
assert_eq!(msgs[1].role, crate::types::Role::Tool);
assert_eq!(msgs[1].text_content(), "72°F");
assert_eq!(msgs[1].metadata["tool_call_id"], "call_1");
}
#[test]
fn test_default_format_tool_messages_multiple() {
let executor = DefaultFormatExecutor;
let tool_calls = vec![
crate::types::ToolCall {
id: "c1".into(),
name: "add".into(),
arguments: "{}".into(),
},
crate::types::ToolCall {
id: "c2".into(),
name: "sub".into(),
arguments: "{}".into(),
},
];
let results = vec!["3".to_string(), "1".to_string()];
let msgs =
executor.format_tool_messages(&serde_json::json!({}), &tool_calls, &results, None);
assert_eq!(msgs.len(), 3);
assert_eq!(msgs[0].role, crate::types::Role::Assistant);
assert_eq!(msgs[1].role, crate::types::Role::Tool);
assert_eq!(msgs[1].text_content(), "3");
assert_eq!(msgs[2].role, crate::types::Role::Tool);
assert_eq!(msgs[2].text_content(), "1");
}
#[test]
fn test_default_format_tool_messages_empty() {
let executor = DefaultFormatExecutor;
let msgs = executor.format_tool_messages(&serde_json::json!({}), &[], &[], None);
assert_eq!(msgs.len(), 1);
assert_eq!(msgs[0].role, crate::types::Role::Assistant);
}
#[test]
fn test_invoker_error_display() {
let err = InvokerError::NotFound {
group: "executor".into(),
key: "test".into(),
};
assert_eq!(err.to_string(), "no executor registered for key 'test'");
let err = InvokerError::Validation("missing field".into());
assert_eq!(err.to_string(), "validation error: missing field");
}
}