pub mod request;
pub mod response;
pub use request::*;
pub use response::*;
use std::marker::PhantomData;
use serde::Serialize;
use crate::ZaiResult;
use crate::client::error::codes;
#[derive(Debug, Clone, Copy, Default)]
pub struct NonStreaming;
#[derive(Debug, Clone, Copy, Default)]
pub struct Streaming;
pub trait StreamMode: private::Sealed {
const WIRE: Option<bool>;
}
impl StreamMode for NonStreaming {
const WIRE: Option<bool> = Some(false);
}
impl StreamMode for Streaming {
const WIRE: Option<bool> = Some(true);
}
mod private {
pub trait Sealed {}
impl Sealed for super::NonStreaming {}
impl Sealed for super::Streaming {}
}
impl<N: StreamMode> AgentInvokeRequest<N> {
pub fn builder(agent_id: impl Into<String>) -> AgentInvokeRequestBuilder<N> {
AgentInvokeRequestBuilder {
agent_id: agent_id.into(),
messages: Vec::new(),
custom_variables: serde_json::Map::new(),
_marker: PhantomData,
}
}
}
pub struct AgentInvokeRequestBuilder<N: StreamMode> {
agent_id: String,
messages: Vec<AgentMessage>,
custom_variables: serde_json::Map<String, serde_json::Value>,
_marker: PhantomData<N>,
}
impl<N: StreamMode> AgentInvokeRequestBuilder<N> {
pub fn message(mut self, msg: AgentMessage) -> Self {
self.messages.push(msg);
self
}
pub fn custom_variables(mut self, vars: serde_json::Map<String, serde_json::Value>) -> Self {
self.custom_variables = vars;
self
}
pub fn build(self) -> ZaiResult<AgentInvokeRequest<N>> {
if self.agent_id.trim().is_empty() {
return Err(crate::ZaiError::ApiError {
code: codes::SDK_VALIDATION,
message: "agent_id must be non-empty".to_string(),
});
}
if self.messages.is_empty() {
return Err(crate::ZaiError::ApiError {
code: codes::SDK_VALIDATION,
message: "AgentInvokeRequest requires at least one message".to_string(),
});
}
for m in &self.messages {
if !matches!(m.role.as_str(), "system" | "user" | "assistant") {
return Err(crate::ZaiError::ApiError {
code: codes::SDK_VALIDATION,
message: format!("invalid message role `{}`", m.role),
});
}
}
Ok(AgentInvokeRequest {
agent_id: self.agent_id,
messages: self.messages,
custom_variables: self.custom_variables,
_marker: PhantomData,
stream: N::WIRE,
})
}
pub fn streaming(self) -> AgentInvokeRequestBuilder<Streaming> {
AgentInvokeRequestBuilder {
agent_id: self.agent_id,
messages: self.messages,
custom_variables: self.custom_variables,
_marker: PhantomData,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct AgentInvokeRequest<N: StreamMode> {
pub agent_id: String,
pub messages: Vec<AgentMessage>,
#[serde(skip_serializing_if = "serde_json::Map::is_empty")]
pub custom_variables: serde_json::Map<String, serde_json::Value>,
#[serde(skip)]
pub _marker: PhantomData<N>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
}
impl<N: StreamMode> AgentInvokeRequest<N> {
pub fn stream_value(&self) -> Option<bool> {
self.stream
}
}
#[derive(Debug, Clone, Serialize)]
pub struct AgentAsyncResultRequest {
pub agent_id: String,
pub async_id: String,
}
impl AgentAsyncResultRequest {
pub fn builder(
agent_id: impl Into<String>,
async_id: impl Into<String>,
) -> AgentAsyncResultRequestBuilder {
AgentAsyncResultRequestBuilder {
agent_id: agent_id.into(),
async_id: async_id.into(),
}
}
}
pub struct AgentAsyncResultRequestBuilder {
agent_id: String,
async_id: String,
}
impl AgentAsyncResultRequestBuilder {
pub fn build(self) -> ZaiResult<AgentAsyncResultRequest> {
if self.agent_id.trim().is_empty() || self.async_id.trim().is_empty() {
return Err(crate::ZaiError::ApiError {
code: codes::SDK_VALIDATION,
message: "agent_id and async_id must both be non-empty".to_string(),
});
}
Ok(AgentAsyncResultRequest {
agent_id: self.agent_id,
async_id: self.async_id,
})
}
}
#[derive(Debug, Clone, Serialize)]
pub struct AgentConversationRequest {
pub agent_id: String,
pub conversation_id: String,
pub messages: Vec<AgentMessage>,
}
impl AgentConversationRequest {
pub fn builder(
agent_id: impl Into<String>,
conversation_id: impl Into<String>,
) -> AgentConversationRequestBuilder {
AgentConversationRequestBuilder {
agent_id: agent_id.into(),
conversation_id: conversation_id.into(),
messages: Vec::new(),
}
}
}
pub struct AgentConversationRequestBuilder {
agent_id: String,
conversation_id: String,
messages: Vec<AgentMessage>,
}
impl AgentConversationRequestBuilder {
pub fn message(mut self, msg: AgentMessage) -> Self {
self.messages.push(msg);
self
}
pub fn build(self) -> ZaiResult<AgentConversationRequest> {
if self.agent_id.trim().is_empty() || self.conversation_id.trim().is_empty() {
return Err(crate::ZaiError::ApiError {
code: codes::SDK_VALIDATION,
message: "agent_id and conversation_id must both be non-empty".to_string(),
});
}
if self.messages.is_empty() {
return Err(crate::ZaiError::ApiError {
code: codes::SDK_VALIDATION,
message: "AgentConversationRequest requires at least one message".to_string(),
});
}
Ok(AgentConversationRequest {
agent_id: self.agent_id,
conversation_id: self.conversation_id,
messages: self.messages,
})
}
}
pub fn message(role: &str, content: impl Into<AgentContent>) -> AgentMessage {
AgentMessage {
role: role.to_string(),
content: content.into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invoke_request_serializes_stream_false_for_nonstreaming() {
let req = AgentInvokeRequest::<NonStreaming>::builder("agent-1")
.message(message("user", "hi"))
.build()
.unwrap();
assert_eq!(req.stream_value(), Some(false));
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["stream"], false);
assert_eq!(json["agent_id"], "agent-1");
}
#[test]
fn invoke_request_serializes_stream_true_for_streaming() {
let req = AgentInvokeRequest::<NonStreaming>::builder("agent-1")
.message(message("user", "hi"))
.streaming()
.build()
.unwrap();
assert_eq!(req.stream_value(), Some(true));
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["stream"], true);
}
#[test]
fn invoke_rejects_empty_agent_id_and_no_messages() {
assert!(
AgentInvokeRequest::<NonStreaming>::builder("")
.message(message("user", "hi"))
.build()
.is_err()
);
assert!(
AgentInvokeRequest::<NonStreaming>::builder("a")
.build()
.is_err()
);
}
#[test]
fn invoke_rejects_invalid_role() {
assert!(
AgentInvokeRequest::<NonStreaming>::builder("a")
.message(message("tool", "hi"))
.build()
.is_err()
);
}
#[test]
fn async_result_rejects_blank_ids() {
assert!(AgentAsyncResultRequest::builder("", "x").build().is_err());
assert!(AgentAsyncResultRequest::builder("x", "").build().is_err());
assert!(AgentAsyncResultRequest::builder("a", "b").build().is_ok());
}
#[test]
fn conversation_rejects_blank_or_no_messages() {
assert!(
AgentConversationRequest::builder("", "c")
.message(message("user", "hi"))
.build()
.is_err()
);
assert!(AgentConversationRequest::builder("a", "c").build().is_err());
assert!(
AgentConversationRequest::builder("a", "c")
.message(message("user", "hi"))
.build()
.is_ok()
);
}
}