diff --git a/crates/agentic-core/src/storage/types/item.rs b/crates/agentic-core/src/storage/types/item.rs index 9da2f44..85a5491 100644 --- a/crates/agentic-core/src/storage/types/item.rs +++ b/crates/agentic-core/src/storage/types/item.rs @@ -90,7 +90,8 @@ impl InOutItem { InOutItem::Input(item) => Some(item), InOutItem::Output(OutputItem::Message(msg)) => Some(InputItem::Message(msg.into())), InOutItem::Output(OutputItem::Reasoning(r)) => Some(InputItem::Reasoning(r)), - InOutItem::Output(OutputItem::FunctionCall(_) | OutputItem::Unknown) => None, + InOutItem::Output(OutputItem::FunctionCall(f)) => Some(InputItem::FunctionCall(f)), + InOutItem::Output(OutputItem::Unknown) => None, }) .collect() } @@ -101,8 +102,8 @@ mod tests { use super::*; use crate::types::event::MessageStatus; use crate::types::io::{ - InputContent, InputMessage, InputMessageContent, OutputMessage, OutputTextContent, ReasoningOutput, - ReasoningTextContent, + FunctionToolCall, InputContent, InputMessage, InputMessageContent, OutputMessage, OutputTextContent, + ReasoningOutput, ReasoningTextContent, }; #[test] @@ -161,11 +162,10 @@ mod tests { InputMessageContent::Parts(parts) => { assert_eq!(parts.len(), 1); match &parts[0] { - InputContent::Text(t) => { - assert_eq!(t.type_, "output_text"); + InputContent::OutputText(t) => { assert_eq!(t.text, "answer"); } - InputContent::Image(_) => panic!("expected text part"), + _ => panic!("expected OutputText part"), } } InputMessageContent::Text(_) => panic!("expected parts content"), @@ -196,6 +196,25 @@ mod tests { } } + #[test] + fn test_into_input_items_preserves_function_calls() { + use crate::types::event::MessageStatus; + let fc = FunctionToolCall { + id: "fc_1".to_string(), + call_id: "call_abc".to_string(), + name: "my_tool".to_string(), + arguments: "{}".to_string(), + status: MessageStatus::Completed, + }; + let items = vec![InOutItem::Output(OutputItem::FunctionCall(fc))]; + let inputs = InOutItem::into_input_items(items); + assert_eq!(inputs.len(), 1); + assert!(matches!(inputs[0], InputItem::FunctionCall(_))); + if let InputItem::FunctionCall(f) = &inputs[0] { + assert_eq!(f.name, "my_tool"); + } + } + #[test] fn test_item_kind_serialization() { let kind = ItemKind::Input; diff --git a/crates/agentic-core/src/types/io/input.rs b/crates/agentic-core/src/types/io/input.rs index cfcbec9..f5df8e2 100644 --- a/crates/agentic-core/src/types/io/input.rs +++ b/crates/agentic-core/src/types/io/input.rs @@ -1,29 +1,36 @@ use serde::{Deserialize, Serialize}; -use super::output::ReasoningOutput; +use super::output::{FunctionToolCall, ReasoningOutput}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InputTextContent { - #[serde(rename = "type")] - pub type_: String, pub text: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InputImageContent { - #[serde(rename = "type")] - pub type_: String, pub image_url: Option, pub detail: Option, } +/// Content item inside a message input. +/// +/// Uses an internally-tagged enum — serde consumes `"type"` for the variant +/// discriminant so the inner structs must NOT redeclare a `type_` field. +/// `output_text` and `reasoning_text` reuse `InputTextContent` since they +/// carry only a `text` field; they are preserved so vLLM sees the full history. #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type")] +#[serde(tag = "type", rename_all = "snake_case")] pub enum InputContent { - #[serde(rename = "input_text")] - Text(InputTextContent), - #[serde(rename = "input_image")] - Image(InputImageContent), + InputText(InputTextContent), + InputImage(InputImageContent), + /// Assistant output text in rehydrated history. + OutputText(InputTextContent), + /// Reasoning step text in rehydrated history. + ReasoningText(InputTextContent), + /// Any other content type — drop silently. + #[serde(other)] + Unknown, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -50,6 +57,10 @@ pub struct FunctionToolResultMessage { pub enum InputItem { #[serde(rename = "message")] Message(InputMessage), + /// The model's tool invocation — appears in rehydrated history so vLLM sees + /// the full call/output pair across turns. + #[serde(rename = "function_call")] + FunctionCall(FunctionToolCall), #[serde(rename = "function_call_output")] FunctionCallOutput(FunctionToolResultMessage), #[serde(rename = "reasoning")] diff --git a/crates/agentic-core/src/types/io/output.rs b/crates/agentic-core/src/types/io/output.rs index 5925f06..fa4a85e 100644 --- a/crates/agentic-core/src/types/io/output.rs +++ b/crates/agentic-core/src/types/io/output.rs @@ -68,12 +68,7 @@ impl From for InputMessage { let parts = msg .content .into_iter() - .map(|c| { - InputContent::Text(InputTextContent { - type_: c.type_, - text: c.text, - }) - }) + .map(|c| InputContent::OutputText(InputTextContent { text: c.text })) .collect(); Self { role: msg.role, @@ -148,6 +143,7 @@ impl ReasoningTextContent { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReasoningOutput { + #[serde(default)] pub id: String, #[serde(default)] pub content: Vec, diff --git a/crates/agentic-core/src/types/io/tools.rs b/crates/agentic-core/src/types/io/tools.rs index 483604f..7e6f61b 100644 --- a/crates/agentic-core/src/types/io/tools.rs +++ b/crates/agentic-core/src/types/io/tools.rs @@ -1,9 +1,13 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; +fn default_function_type() -> String { + "function".to_string() +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FunctionTool { - #[serde(rename = "type")] + #[serde(rename = "type", default = "default_function_type")] pub type_: String, pub name: String, pub description: Option,