use autoagents_llm::chat::StructuredOutputFormat;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AgentInput {
pub system_prompt: String,
pub user_message: String,
pub max_turns: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_schema: Option<StructuredOutputFormat>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum StopReason {
FinalAnswer,
MaxTurnsReached,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentOutput {
pub final_answer: String,
pub stop_reason: StopReason,
pub turns_used: u32,
pub tool_calls: u32,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Role {
System,
User,
Assistant,
Tool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub args: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolResult {
pub call_id: String,
pub output: serde_json::Value,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Message {
pub role: Role,
#[serde(default)]
pub content: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<ToolCall>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
impl Message {
pub fn system(content: impl Into<String>) -> Self {
Self {
role: Role::System,
content: content.into(),
tool_calls: vec![],
tool_call_id: None,
}
}
pub fn user(content: impl Into<String>) -> Self {
Self {
role: Role::User,
content: content.into(),
tool_calls: vec![],
tool_call_id: None,
}
}
pub fn assistant_text(content: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: content.into(),
tool_calls: vec![],
tool_call_id: None,
}
}
pub fn assistant_with_tools(calls: Vec<ToolCall>) -> Self {
Self {
role: Role::Assistant,
content: String::new(),
tool_calls: calls,
tool_call_id: None,
}
}
pub fn tool_result(result: &ToolResult) -> Self {
let content = match &result.error {
Some(err) => format!("ERROR: {err}"),
None => result.output.to_string(),
};
Self {
role: Role::Tool,
content,
tool_calls: vec![],
tool_call_id: Some(result.call_id.clone()),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AgentState {
pub input: AgentInput,
pub history: Vec<Message>,
pub turn: u32,
pub tool_calls_executed: u32,
#[serde(default)]
pub pending_user_messages: Vec<String>,
}
impl AgentState {
pub fn new(input: AgentInput) -> Self {
let history = vec![
Message::system(&input.system_prompt),
Message::user(&input.user_message),
];
Self {
input,
history,
turn: 0,
tool_calls_executed: 0,
pending_user_messages: vec![],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum LlmResponse {
Final { answer: String },
UseTools { calls: Vec<ToolCall> },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmChatInput {
pub messages: Vec<Message>,
pub tools: Vec<ToolSchema>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_schema: Option<StructuredOutputFormat>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolSchema {
pub name: String,
pub description: String,
pub args_schema: serde_json::Value,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_state_seeds_system_and_user() {
let s = AgentState::new(AgentInput {
system_prompt: "be helpful".into(),
user_message: "hi".into(),
max_turns: 5,
output_schema: None,
});
assert_eq!(s.history.len(), 2);
assert_eq!(s.history[0].role, Role::System);
assert_eq!(s.history[1].role, Role::User);
assert_eq!(s.turn, 0);
}
#[test]
fn message_roundtrips_through_json() {
let m = Message::assistant_with_tools(vec![ToolCall {
id: "c1".into(),
name: "add".into(),
args: serde_json::json!({"a": 1, "b": 2}),
}]);
let s = serde_json::to_string(&m).unwrap();
let back: Message = serde_json::from_str(&s).unwrap();
assert_eq!(m, back);
}
fn sample_schema() -> StructuredOutputFormat {
StructuredOutputFormat {
name: "weather_report".into(),
description: Some("Structured weather observation".into()),
schema: Some(serde_json::json!({
"type": "object",
"properties": {
"city": { "type": "string" },
"temperature_c": { "type": "number" },
},
"required": ["city", "temperature_c"]
})),
strict: Some(true),
}
}
#[test]
fn llm_chat_input_roundtrips_with_schema() {
let input = LlmChatInput {
messages: vec![Message::user("hello")],
tools: vec![ToolSchema {
name: "noop".into(),
description: "does nothing".into(),
args_schema: serde_json::json!({"type": "object"}),
}],
output_schema: Some(sample_schema()),
};
let s = serde_json::to_string(&input).unwrap();
let back: LlmChatInput = serde_json::from_str(&s).unwrap();
assert_eq!(back.messages, input.messages);
assert_eq!(back.output_schema, input.output_schema);
}
#[test]
fn llm_chat_input_omits_absent_schema() {
let input = LlmChatInput {
messages: vec![],
tools: vec![],
output_schema: None,
};
let s = serde_json::to_string(&input).unwrap();
assert!(!s.contains("output_schema"), "got {s}");
}
#[test]
fn agent_input_deserializes_without_schema_field() {
let legacy = r#"{"system_prompt":"s","user_message":"u","max_turns":3}"#;
let parsed: AgentInput = serde_json::from_str(legacy).unwrap();
assert_eq!(parsed.max_turns, 3);
assert!(parsed.output_schema.is_none());
}
}