use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct AgentCard {
#[cfg_attr(feature = "openapi", schema(example = "Support agent"))]
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub streaming: bool,
pub input: AgentCardInput,
#[serde(default)]
pub auth: Vec<AgentCardAuth>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub conversation_starters: Vec<String>,
pub links: AgentCardLinks,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct AgentCardInput {
pub text: bool,
pub images: bool,
pub files: bool,
}
impl AgentCardInput {
pub const TEXT: Self = Self {
text: true,
images: false,
files: false,
};
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AgentCardAuth {
AgentKey,
Oidc {
issuer: String,
},
OAuth2,
RuntimeToken,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct AgentCardLinks {
pub sessions: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ag_ui: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub a2a: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct CreateAgentSessionRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum ToolApprovalDecision {
Allow,
AllowAlways,
Reject,
RejectAlways,
}
impl ToolApprovalDecision {
pub fn allows(self) -> bool {
matches!(self, Self::Allow | Self::AllowAlways)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ToolApprovalAnswer {
#[cfg_attr(
feature = "openapi",
schema(example = "tool_approval_toolu_01933b5a00007000800000000000001")
)]
pub tool_call_id: String,
pub decision: ToolApprovalDecision,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct SubmitToolApprovalsRequest {
pub decisions: Vec<ToolApprovalAnswer>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct ToolApprovalResolution {
pub tool_call_id: String,
pub tool: String,
#[cfg_attr(feature = "openapi", schema(example = "allow"))]
pub outcome: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct SubmitToolApprovalsResponse {
pub resolved: Vec<ToolApprovalResolution>,
#[cfg_attr(feature = "openapi", schema(example = "active"))]
pub status: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_kinds_serialize_as_tagged_objects() {
let auth = vec![
AgentCardAuth::AgentKey,
AgentCardAuth::Oidc {
issuer: "https://auth.example.com".into(),
},
];
assert_eq!(
serde_json::to_value(&auth).unwrap(),
serde_json::json!([
{"type": "agent_key"},
{"type": "oidc", "issuer": "https://auth.example.com"}
])
);
}
#[test]
fn approval_request_round_trips_the_server_shape() {
let body = serde_json::json!({
"decisions": [{"tool_call_id": "tool_approval_1", "decision": "allow_always"}]
});
let parsed: SubmitToolApprovalsRequest = serde_json::from_value(body.clone()).unwrap();
assert!(parsed.decisions[0].decision.allows());
assert_eq!(serde_json::to_value(&parsed).unwrap(), body);
}
#[test]
fn card_omits_empty_optional_fields() {
let card = AgentCard {
name: "support".into(),
description: None,
streaming: true,
input: AgentCardInput::TEXT,
auth: vec![],
conversation_starters: vec![],
links: AgentCardLinks {
sessions: "/v1/channels/support/sessions".into(),
ag_ui: None,
a2a: None,
},
};
let json = serde_json::to_value(&card).unwrap();
assert!(json.get("description").is_none());
assert!(json.get("conversation_starters").is_none());
assert!(json["links"].get("ag_ui").is_none());
}
}