pub use agent_client_protocol::schema::ProtocolVersion;
#[cfg(test)]
pub(crate) use agent_client_protocol::schema::v1::ImageContent;
pub use agent_client_protocol::schema::v1::{
AgentCapabilities, AuthenticateRequest as AuthenticateParams,
AuthenticateResponse as AuthenticateResult, AvailableCommand, AvailableCommandInput,
AvailableCommandsUpdate, CancelNotification, Content, ContentBlock, ContentChunk,
CurrentModeUpdate, EmbeddedResource, EmbeddedResourceResource,
InitializeRequest as InitializeParams, InitializeResponse as InitializeResult,
LoadSessionRequest as LoadSessionParams, LoadSessionResponse as LoadSessionResult,
McpCapabilities, McpServer, NewSessionRequest as NewSessionParams,
NewSessionResponse as NewSessionResult, PermissionOption, PermissionOptionKind, Plan,
PlanEntry, PlanEntryPriority, PlanEntryStatus, PromptCapabilities,
PromptRequest as PromptParams, PromptResponse as PromptResult, RequestPermissionOutcome,
RequestPermissionRequest as RequestPermissionParams, ResourceLink, SessionMode, SessionModeId,
SessionModeState, SessionNotification, SessionUpdate,
SetSessionModeRequest as SetSessionModeParams, SetSessionModeResponse as SetSessionModeResult,
StopReason, TextContent, ToolCall, ToolCallContent, ToolCallStatus, ToolCallUpdate,
ToolCallUpdateFields, ToolKind, UnstructuredCommandInput,
};
use serde_json::{Map, Value};
pub const PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::V1;
pub fn text_block(text: impl Into<String>) -> ContentBlock {
ContentBlock::Text(TextContent::new(text))
}
pub fn text_chunk(text: impl Into<String>) -> ContentChunk {
ContentChunk::new(text_block(text))
}
pub fn content(text: impl Into<String>) -> ToolCallContent {
ToolCallContent::Content(Content::new(text_block(text)))
}
pub fn meta(value: Value) -> Option<Map<String, Value>> {
match value {
Value::Object(map) => Some(map),
_ => None,
}
}
pub fn prompt_text(blocks: &[ContentBlock]) -> String {
let mut parts = Vec::new();
for block in blocks {
if let ContentBlock::Text(text) = block {
parts.push(text.text.clone());
}
}
parts.join("\n")
}
pub fn prompt_model_text(blocks: &[ContentBlock]) -> String {
let mut parts = Vec::new();
for block in blocks {
match block {
ContentBlock::Text(text) => parts.push(text.text.clone()),
ContentBlock::Resource(resource) => parts.push(embedded_resource_text(resource)),
ContentBlock::ResourceLink(link) => parts.push(resource_link_text(link)),
_ => {}
}
}
parts.join("\n")
}
fn embedded_resource_text(resource: &EmbeddedResource) -> String {
match &resource.resource {
EmbeddedResourceResource::TextResourceContents(text) => {
format!(
"<resource uri=\"{}\">\n{}\n</resource>",
text.uri, text.text
)
}
EmbeddedResourceResource::BlobResourceContents(blob) => format!(
"[embedded resource {} omitted: {} binary content]",
blob.uri,
blob.mime_type.as_deref().unwrap_or("unknown type")
),
_ => "[embedded resource of an unsupported kind omitted]".to_string(),
}
}
fn resource_link_text(link: &ResourceLink) -> String {
match link.description.as_deref().filter(|d| !d.is_empty()) {
Some(description) => {
format!(
"[linked resource: {} ({}) — {description}]",
link.name, link.uri
)
}
None => format!("[linked resource: {} ({})]", link.name, link.uri),
}
}
#[cfg(test)]
mod tests {
use super::*;
use agent_client_protocol::schema::v1::ImageContent;
#[test]
fn sdk_initialize_result_serializes_camel_case() {
let result = InitializeResult::new(PROTOCOL_VERSION).agent_capabilities(
AgentCapabilities::new()
.load_session(true)
.prompt_capabilities(PromptCapabilities::new().embedded_context(true)),
);
let v = serde_json::to_value(&result).unwrap();
assert_eq!(v["protocolVersion"], 1);
assert_eq!(v["agentCapabilities"]["loadSession"], true);
assert_eq!(
v["agentCapabilities"]["promptCapabilities"]["embeddedContext"],
true
);
assert!(v["authMethods"].as_array().unwrap().is_empty());
}
#[test]
fn sdk_message_chunk_uses_snake_case_discriminator() {
let update = SessionUpdate::AgentMessageChunk(text_chunk("hi"));
let v = serde_json::to_value(&update).unwrap();
assert_eq!(v["sessionUpdate"], "agent_message_chunk");
assert_eq!(v["content"]["type"], "text");
assert_eq!(v["content"]["text"], "hi");
}
#[test]
fn prompt_text_concatenates_text_blocks_only() {
let blocks = vec![
text_block("hello"),
ContentBlock::Image(ImageContent::new("image/png", "...")),
text_block("world"),
];
assert_eq!(prompt_text(&blocks), "hello\nworld");
}
#[test]
fn prompt_model_text_folds_embedded_text_resource() {
use agent_client_protocol::schema::v1::{
EmbeddedResource, EmbeddedResourceResource, TextResourceContents,
};
let blocks = vec![
text_block("check this file"),
ContentBlock::Resource(EmbeddedResource::new(
EmbeddedResourceResource::TextResourceContents(TextResourceContents::new(
"fn main() {}",
"file:///main.rs",
)),
)),
];
let text = prompt_model_text(&blocks);
assert!(text.contains("check this file"));
assert!(text.contains("file:///main.rs"));
assert!(text.contains("fn main() {}"));
assert_eq!(prompt_text(&blocks), "check this file");
}
#[test]
fn prompt_model_text_references_blob_and_link_without_inlining_bytes() {
use agent_client_protocol::schema::v1::{
BlobResourceContents, EmbeddedResource, EmbeddedResourceResource, ResourceLink,
};
let blocks = vec![
ContentBlock::Resource(EmbeddedResource::new(
EmbeddedResourceResource::BlobResourceContents(BlobResourceContents::new(
"QUJD",
"file:///logo.png",
)),
)),
ContentBlock::ResourceLink(ResourceLink::new("Design doc", "https://example.com/doc")),
];
let text = prompt_model_text(&blocks);
assert!(text.contains("file:///logo.png"));
assert!(!text.contains("QUJD"), "binary blob must not be inlined");
assert!(text.contains("Design doc"));
assert!(text.contains("https://example.com/doc"));
}
}