Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions crates/agentic-core/src/storage/types/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand All @@ -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]
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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;
Expand Down
31 changes: 21 additions & 10 deletions crates/agentic-core/src/types/io/input.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pub detail: Option<String>,
}

/// 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)]
Expand All @@ -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")]
Expand Down
8 changes: 2 additions & 6 deletions crates/agentic-core/src/types/io/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,7 @@ impl From<OutputMessage> 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,
Expand Down Expand Up @@ -148,6 +143,7 @@ impl ReasoningTextContent {

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReasoningOutput {
#[serde(default)]
pub id: String,
#[serde(default)]
pub content: Vec<ReasoningTextContent>,
Expand Down
6 changes: 5 additions & 1 deletion crates/agentic-core/src/types/io/tools.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
Expand Down