1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4pub mod utils;
6
7pub mod skill_name;
9pub use skill_name::{
10 is_valid_skill_name, normalize_mcp_server_segment, parse_skill_name,
11 synthesize_marketplace_name, synthesize_mcp_name, synthesize_user_name, ParsedSkillName,
12 SkillNameError, SkillNameKind,
13};
14
15pub mod version;
17pub use version::{
18 is_compatible, ProtocolVersion, ProtocolVersionError, ProtocolVersionParseError,
19};
20
21pub const SMCP_NAMESPACE: &str = "/smcp";
23
24pub const PROTOCOL_VERSION: &str = "0.2.0";
36
37pub mod error_codes {
46 pub const BAD_REQUEST: i32 = 400;
48 pub const UNAUTHORIZED: i32 = 401;
49 pub const FORBIDDEN: i32 = 403;
50 pub const NOT_FOUND: i32 = 404;
51 pub const TIMEOUT: i32 = 408;
52 pub const INTERNAL_ERROR: i32 = 500;
53
54 pub const TOOL_NOT_FOUND: i32 = 4001;
64 pub const TOOL_DISABLED: i32 = 4002;
65 pub const TOOL_EXECUTION_FAILED: i32 = 4003;
71 pub const TOOL_TIMEOUT: i32 = 4004;
72 pub const TOOL_REQUIRES_CONFIRMATION: i32 = 4005;
73
74 pub const ROOM_FULL: i32 = 4101;
76 pub const ROOM_NOT_FOUND: i32 = 4102;
77 pub const NOT_IN_ROOM: i32 = 4103;
78 pub const CROSS_ROOM_ACCESS: i32 = 4104;
79}
80
81pub const WS_VERSION_HANDSHAKE_REJECTED_CLOSE_CODE: i32 = 4900;
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
121#[repr(i32)]
122pub enum ErrorCode {
123 NotFound = 404,
125 ToolAuthorizationRequired = 4006,
127 ToolAuthorizationFailed = 4007,
129 ProtocolVersionMismatch = 4008,
133 McpServerNotFound = 4014,
135 McpCapabilityNotSupported = 4015,
137 SkillNameInvalid = 4016,
139 SkillResourceNotAccessible = 4017,
141 BlobNotAccessible = 4018,
143}
144
145impl ErrorCode {
146 pub const fn code(self) -> i32 {
148 self as i32
149 }
150
151 pub fn from_code(code: i32) -> Option<Self> {
153 match code {
154 404 => Some(Self::NotFound),
155 4006 => Some(Self::ToolAuthorizationRequired),
156 4007 => Some(Self::ToolAuthorizationFailed),
157 4008 => Some(Self::ProtocolVersionMismatch),
158 4014 => Some(Self::McpServerNotFound),
159 4015 => Some(Self::McpCapabilityNotSupported),
160 4016 => Some(Self::SkillNameInvalid),
161 4017 => Some(Self::SkillResourceNotAccessible),
162 4018 => Some(Self::BlobNotAccessible),
163 _ => None,
164 }
165 }
166}
167
168impl Serialize for ErrorCode {
169 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
170 where
171 S: serde::Serializer,
172 {
173 serializer.serialize_i32(self.code())
174 }
175}
176
177impl<'de> Deserialize<'de> for ErrorCode {
178 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
179 where
180 D: serde::Deserializer<'de>,
181 {
182 let code = i32::deserialize(deserializer)?;
183 Self::from_code(code)
184 .ok_or_else(|| serde::de::Error::custom(format!("unknown A2C-SMCP error code: {code}")))
185 }
186}
187
188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
213pub struct ErrorPayload {
214 pub code: i64,
216 pub message: String,
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub details: Option<serde_json::Value>,
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub server_version: Option<String>,
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub client_version: Option<String>,
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub min_supported: Option<String>,
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub max_supported: Option<String>,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub mcp_server_name: Option<String>,
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub capability: Option<String>,
239 #[serde(flatten)]
243 pub extra: serde_json::Map<String, serde_json::Value>,
244}
245
246impl ErrorPayload {
247 pub fn new(code: i64, message: impl Into<String>) -> Self {
249 Self {
250 code,
251 message: message.into(),
252 details: None,
253 server_version: None,
254 client_version: None,
255 min_supported: None,
256 max_supported: None,
257 mcp_server_name: None,
258 capability: None,
259 extra: serde_json::Map::new(),
260 }
261 }
262
263 pub fn from_error_code(code: ErrorCode, message: impl Into<String>) -> Self {
273 Self::new(i64::from(code.code()), message)
274 }
275
276 pub fn with_details(mut self, details: serde_json::Value) -> Self {
278 self.details = Some(details);
279 self
280 }
281
282 pub fn with_detail(
285 mut self,
286 key: impl Into<String>,
287 value: impl Into<serde_json::Value>,
288 ) -> Self {
289 let mut map = match self.details {
290 Some(serde_json::Value::Object(map)) => map,
291 _ => serde_json::Map::new(),
292 };
293 map.insert(key.into(), value.into());
294 self.details = Some(serde_json::Value::Object(map));
295 self
296 }
297
298 pub fn with_mcp_server_name(mut self, name: impl Into<String>) -> Self {
300 self.mcp_server_name = Some(name.into());
301 self
302 }
303
304 pub fn with_capability(mut self, capability: impl Into<String>) -> Self {
306 self.capability = Some(capability.into());
307 self
308 }
309
310 pub fn version_mismatch(client: &ProtocolVersion, server: &ProtocolVersion) -> Self {
316 Self {
317 code: i64::from(ErrorCode::ProtocolVersionMismatch.code()),
318 message: "Protocol version mismatch".to_string(),
319 details: None,
320 server_version: Some(server.to_string()),
321 client_version: Some(client.to_string()),
322 min_supported: Some(format!("{}.{}.0", server.major, server.minor)),
323 max_supported: Some(format!("{}.{}.999", server.major, server.minor)),
324 mcp_server_name: None,
325 capability: None,
326 extra: serde_json::Map::new(),
327 }
328 }
329}
330
331pub fn is_protocol_error_payload(value: &serde_json::Value) -> bool {
343 value
344 .as_object()
345 .and_then(|obj| obj.get("code"))
346 .and_then(serde_json::Value::as_i64)
347 .and_then(|code| i32::try_from(code).ok())
348 .is_some_and(|code| ErrorCode::from_code(code).is_some())
349}
350
351pub fn build_computer_not_found_error(computer_name: &str) -> ErrorPayload {
359 ErrorPayload::new(
360 i64::from(ErrorCode::NotFound.code()),
361 format!("Computer with name '{computer_name}' not found"),
362 )
363 .with_detail("computer_name", computer_name)
364}
365
366pub mod events {
368 pub const CLIENT_GET_TOOLS: &str = "client:get_tools";
370 pub const CLIENT_GET_CONFIG: &str = "client:get_config";
372 pub const CLIENT_GET_DESKTOP: &str = "client:get_desktop";
374 pub const CLIENT_TOOL_CALL: &str = "client:tool_call";
376 pub const CLIENT_GET_RESOURCES: &str = "client:get_resources";
378 pub const CLIENT_GET_SKILLS: &str = "client:get_skills";
380 pub const CLIENT_GET_SKILL: &str = "client:get_skill";
382 pub const CLIENT_GET_BLOB: &str = "client:get_blob";
384
385 pub const SERVER_JOIN_OFFICE: &str = "server:join_office";
387 pub const SERVER_LEAVE_OFFICE: &str = "server:leave_office";
389 pub const SERVER_UPDATE_CONFIG: &str = "server:update_config";
391 pub const SERVER_UPDATE_TOOL_LIST: &str = "server:update_tool_list";
393 pub const SERVER_UPDATE_DESKTOP: &str = "server:update_desktop";
395 pub const SERVER_UPDATE_SKILLS: &str = "server:update_skills";
397 pub const SERVER_TOOL_CALL_CANCEL: &str = "server:tool_call_cancel";
399 pub const SERVER_LIST_ROOM: &str = "server:list_room";
401
402 pub const NOTIFY_TOOL_CALL_CANCEL: &str = "notify:tool_call_cancel";
404 pub const NOTIFY_ENTER_OFFICE: &str = "notify:enter_office";
406 pub const NOTIFY_LEAVE_OFFICE: &str = "notify:leave_office";
408 pub const NOTIFY_UPDATE_CONFIG: &str = "notify:update_config";
410 pub const NOTIFY_UPDATE_TOOL_LIST: &str = "notify:update_tool_list";
412 pub const NOTIFY_UPDATE_DESKTOP: &str = "notify:update_desktop";
414 pub const NOTIFY_UPDATE_SKILLS: &str = "notify:update_skills";
416
417 pub const NOTIFY_PREFIX: &str = "notify:";
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
423pub struct ReqId(pub String);
424
425impl ReqId {
426 pub fn new() -> Self {
428 Self(Uuid::new_v4().simple().to_string())
429 }
430
431 pub fn from_string(s: String) -> Self {
433 Self(s)
434 }
435
436 pub fn as_str(&self) -> &str {
438 &self.0
439 }
440}
441
442impl Default for ReqId {
443 fn default() -> Self {
444 Self::new()
445 }
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
450#[serde(rename_all = "lowercase")]
451pub enum Role {
452 Agent,
453 Computer,
454}
455
456impl std::fmt::Display for Role {
457 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
458 match self {
459 Role::Agent => write!(f, "agent"),
460 Role::Computer => write!(f, "computer"),
461 }
462 }
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct UserInfo {
468 pub name: String,
469 pub role: Role,
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize)]
474pub struct ToolCallReq {
475 #[serde(flatten)]
476 pub base: AgentCallData,
477 pub computer: String,
478 pub tool_name: String,
479 pub params: serde_json::Value,
480 pub timeout: i32,
481}
482
483#[derive(Debug, Clone, Serialize, Deserialize)]
485pub struct GetComputerConfigReq {
486 #[serde(flatten)]
487 pub base: AgentCallData,
488 pub computer: String,
489}
490
491#[derive(Debug, Clone, Serialize, Deserialize)]
493pub struct UpdateComputerConfigReq {
494 pub computer: String,
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct GetComputerConfigRet {
500 #[serde(skip_serializing_if = "Option::is_none")]
501 pub inputs: Option<Vec<serde_json::Value>>,
502 pub servers: serde_json::Value,
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct ToolCallRet {
513 #[serde(skip_serializing_if = "Option::is_none")]
514 pub content: Option<Vec<serde_json::Value>>,
515 #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
516 pub is_error: Option<bool>,
517 #[serde(skip_serializing_if = "Option::is_none")]
518 pub req_id: Option<ReqId>,
519 #[serde(skip_serializing_if = "Option::is_none")]
527 pub meta: Option<serde_json::Value>,
528}
529
530pub mod tool_meta {
538 pub const A2C_CANCELLED_KEY: &str = "a2c_cancelled";
540 pub const A2C_CANCEL_REASON_KEY: &str = "a2c_cancel_reason";
542 pub const A2C_TIMEOUT_KEY: &str = "a2c_timeout";
544 pub const A2C_DEFAULT_CANCEL_REASON: &str = "agent_requested";
546 pub const A2C_BLOB_HANDLE_KEY: &str = "a2c_blob_handle";
548 pub const A2C_TOTAL_SIZE_KEY: &str = "a2c_total_size";
550 pub const A2C_SHA256_KEY: &str = "a2c_sha256";
552
553 pub const AUTH_ERROR_CODE_KEY: &str = "error_code";
561 pub const AUTH_MCP_SERVER_KEY: &str = "mcp_server";
563 pub const AUTH_HINT_KEY: &str = "auth_hint";
566}
567
568impl ToolCallRet {
569 fn meta_object_mut(&mut self) -> &mut serde_json::Map<String, serde_json::Value> {
570 if !matches!(self.meta, Some(serde_json::Value::Object(_))) {
571 self.meta = Some(serde_json::Value::Object(serde_json::Map::new()));
572 }
573 match self.meta.as_mut() {
574 Some(serde_json::Value::Object(map)) => map,
575 _ => unreachable!("meta 刚被规整为对象"),
576 }
577 }
578
579 pub fn mark_cancelled(&mut self, reason: Option<&str>) {
583 let reason = reason
584 .unwrap_or(tool_meta::A2C_DEFAULT_CANCEL_REASON)
585 .to_string();
586 let map = self.meta_object_mut();
587 map.insert(tool_meta::A2C_CANCELLED_KEY.to_string(), true.into());
588 map.insert(tool_meta::A2C_CANCEL_REASON_KEY.to_string(), reason.into());
589 }
590
591 pub fn mark_timeout(&mut self) {
593 self.meta_object_mut()
594 .insert(tool_meta::A2C_TIMEOUT_KEY.to_string(), true.into());
595 }
596
597 pub fn is_cancelled(&self) -> bool {
599 meta_bool(self.meta.as_ref(), tool_meta::A2C_CANCELLED_KEY)
600 }
601
602 pub fn cancel_reason(&self) -> Option<&str> {
604 self.meta
605 .as_ref()
606 .and_then(|m| m.get(tool_meta::A2C_CANCEL_REASON_KEY))
607 .and_then(serde_json::Value::as_str)
608 }
609
610 pub fn is_timeout(&self) -> bool {
612 meta_bool(self.meta.as_ref(), tool_meta::A2C_TIMEOUT_KEY)
613 }
614}
615
616fn meta_bool(meta: Option<&serde_json::Value>, key: &str) -> bool {
617 meta.and_then(|m| m.get(key))
618 .and_then(serde_json::Value::as_bool)
619 .unwrap_or(false)
620}
621
622#[derive(Debug, Clone, PartialEq, Eq)]
624pub struct BlobSideband {
625 pub blob_handle: BlobHandle,
627 pub total_size: Option<u64>,
629 pub sha256: Option<String>,
631}
632
633pub fn set_content_blob_sideband(
638 item: &mut serde_json::Value,
639 blob_handle: &str,
640 total_size: Option<u64>,
641 sha256: Option<&str>,
642) {
643 let serde_json::Value::Object(obj) = item else {
644 return;
645 };
646 let meta = obj
647 .entry("_meta")
648 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
649 if !meta.is_object() {
650 *meta = serde_json::Value::Object(serde_json::Map::new());
651 }
652 if let serde_json::Value::Object(meta) = meta {
653 meta.insert(
654 tool_meta::A2C_BLOB_HANDLE_KEY.to_string(),
655 blob_handle.into(),
656 );
657 if let Some(size) = total_size {
658 meta.insert(tool_meta::A2C_TOTAL_SIZE_KEY.to_string(), size.into());
659 }
660 if let Some(hash) = sha256 {
661 meta.insert(tool_meta::A2C_SHA256_KEY.to_string(), hash.into());
662 }
663 }
664}
665
666pub fn read_content_blob_sideband(item: &serde_json::Value) -> Option<BlobSideband> {
671 let meta = item.get("_meta")?;
672 let blob_handle = meta
673 .get(tool_meta::A2C_BLOB_HANDLE_KEY)
674 .and_then(serde_json::Value::as_str)
675 .filter(|s| !s.is_empty())?;
676 Some(BlobSideband {
677 blob_handle: blob_handle.to_string(),
678 total_size: meta
679 .get(tool_meta::A2C_TOTAL_SIZE_KEY)
680 .and_then(serde_json::Value::as_u64),
681 sha256: meta
682 .get(tool_meta::A2C_SHA256_KEY)
683 .and_then(serde_json::Value::as_str)
684 .map(str::to_string),
685 })
686}
687
688#[derive(Debug, Clone, Serialize, Deserialize)]
690pub struct GetToolsReq {
691 #[serde(flatten)]
692 pub base: AgentCallData,
693 pub computer: String,
694}
695
696#[derive(Debug, Clone, Serialize, Deserialize)]
698pub struct SMCPTool {
699 pub name: String,
700 pub description: String,
701 pub params_schema: serde_json::Value,
702 pub return_schema: Option<serde_json::Value>,
703 #[serde(skip_serializing_if = "Option::is_none")]
704 pub meta: Option<serde_json::Value>,
705}
706
707#[derive(Debug, Clone, Serialize, Deserialize)]
709pub struct GetToolsRet {
710 pub tools: Vec<SMCPTool>,
711 pub req_id: ReqId,
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
716pub struct AgentCallData {
717 pub agent: String,
718 pub req_id: ReqId,
719}
720
721#[derive(Debug, Clone, Serialize, Deserialize)]
723pub struct EnterOfficeReq {
724 pub role: Role,
725 pub name: String,
726 pub office_id: String,
727}
728
729#[derive(Debug, Clone, Serialize, Deserialize)]
731pub struct LeaveOfficeReq {
732 pub office_id: String,
733}
734
735#[derive(Debug, Clone, Serialize, Deserialize)]
737pub struct GetDesktopReq {
738 #[serde(flatten)]
739 pub base: AgentCallData,
740 pub computer: String,
741 #[serde(skip_serializing_if = "Option::is_none")]
742 pub desktop_size: Option<i32>,
743 #[serde(skip_serializing_if = "Option::is_none")]
744 pub window: Option<String>,
745}
746
747pub type Desktop = String;
749
750#[derive(Debug, Clone, Serialize, Deserialize)]
752pub struct GetDesktopRet {
753 #[serde(skip_serializing_if = "Option::is_none")]
754 pub desktops: Option<Vec<Desktop>>,
755 pub req_id: ReqId,
756}
757
758pub const GET_RESOURCES_EVENT: &str = events::CLIENT_GET_RESOURCES;
770
771#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
775#[serde(rename_all = "lowercase")]
776pub enum ResourceAudience {
777 User,
779 Assistant,
781}
782
783#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
788pub struct ResourceAnnotations {
789 #[serde(default, skip_serializing_if = "Option::is_none")]
791 pub audience: Option<Vec<ResourceAudience>>,
792 #[serde(default, skip_serializing_if = "Option::is_none")]
794 pub priority: Option<f32>,
795 #[serde(default, skip_serializing_if = "Option::is_none")]
797 pub last_modified: Option<String>,
798}
799
800#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
806pub struct A2CResource {
807 #[serde(default, skip_serializing_if = "Option::is_none")]
809 pub uri: Option<String>,
810 #[serde(default, skip_serializing_if = "Option::is_none")]
812 pub name: Option<String>,
813 #[serde(default, skip_serializing_if = "Option::is_none")]
815 pub description: Option<String>,
816 #[serde(default, skip_serializing_if = "Option::is_none")]
818 pub mime_type: Option<String>,
819 #[serde(default, skip_serializing_if = "Option::is_none")]
821 pub size: Option<u64>,
822 #[serde(default, skip_serializing_if = "Option::is_none")]
824 pub annotations: Option<ResourceAnnotations>,
825 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
827 pub meta: Option<serde_json::Value>,
828}
829
830#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
834pub struct GetResourcesReq {
835 #[serde(flatten)]
836 pub base: AgentCallData,
837 pub computer: String,
839 pub mcp_server: String,
841 #[serde(default, skip_serializing_if = "Option::is_none")]
843 pub cursor: Option<String>,
844}
845
846#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
850pub struct GetResourcesRet {
851 #[serde(default)]
853 pub resources: Vec<A2CResource>,
854 #[serde(default, skip_serializing_if = "Option::is_none")]
856 pub next_cursor: Option<String>,
857 #[serde(default, skip_serializing_if = "Option::is_none")]
859 pub req_id: Option<ReqId>,
860}
861
862#[derive(Debug, Clone, Serialize, Deserialize)]
864pub struct ListRoomReq {
865 #[serde(flatten)]
866 pub base: AgentCallData,
867 pub office_id: String,
868}
869
870#[derive(Debug, Clone, Serialize, Deserialize)]
872pub struct SessionInfo {
873 pub sid: String,
874 pub name: String,
875 pub role: Role,
876 pub office_id: String,
877 #[serde(default, skip_serializing_if = "Option::is_none")]
885 pub a2c_version: Option<String>,
886}
887
888#[derive(Debug, Clone, Serialize, Deserialize)]
890pub struct ListRoomRet {
891 pub sessions: Vec<SessionInfo>,
892 pub req_id: ReqId,
893}
894
895#[derive(Debug, Clone, Serialize, Deserialize)]
897pub struct EnterOfficeNotification {
898 pub office_id: String,
899 #[serde(skip_serializing_if = "Option::is_none")]
900 pub computer: Option<String>,
901 #[serde(skip_serializing_if = "Option::is_none")]
902 pub agent: Option<String>,
903}
904
905#[derive(Debug, Clone, Serialize, Deserialize)]
907pub struct LeaveOfficeNotification {
908 pub office_id: String,
909 #[serde(skip_serializing_if = "Option::is_none")]
910 pub computer: Option<String>,
911 #[serde(skip_serializing_if = "Option::is_none")]
912 pub agent: Option<String>,
913}
914
915#[derive(Debug, Clone, Serialize, Deserialize)]
917pub struct UpdateMCPConfigNotification {
918 pub computer: String,
919}
920
921#[derive(Debug, Clone, Serialize, Deserialize)]
923pub struct UpdateToolListNotification {
924 pub computer: String,
925}
926
927#[derive(Debug, Clone, Serialize, Deserialize)]
929#[serde(tag = "type")]
930pub enum Notification {
931 ToolCallCancel,
932 EnterOffice(EnterOfficeNotification),
933 LeaveOffice(LeaveOfficeNotification),
934 UpdateMCPConfig(UpdateMCPConfigNotification),
935 UpdateToolList(UpdateToolListNotification),
936 UpdateDesktop,
937}
938
939pub const GET_SKILLS_EVENT: &str = events::CLIENT_GET_SKILLS;
950pub const GET_SKILL_EVENT: &str = events::CLIENT_GET_SKILL;
952pub const UPDATE_SKILLS_EVENT: &str = events::SERVER_UPDATE_SKILLS;
954pub const UPDATE_SKILLS_NOTIFICATION: &str = events::NOTIFY_UPDATE_SKILLS;
956
957#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
968pub struct A2CSkillRef {
969 pub name: String,
971 pub source: String,
973 #[serde(default, skip_serializing_if = "Option::is_none")]
975 pub uri: Option<String>,
976 pub path: String,
978 pub description: String,
980 #[serde(default, skip_serializing_if = "Option::is_none")]
982 pub license: Option<String>,
983 #[serde(default, skip_serializing_if = "Option::is_none")]
985 pub compatibility: Option<String>,
986 #[serde(default, skip_serializing_if = "Option::is_none")]
988 pub allowed_tools: Option<Vec<String>>,
989 #[serde(default, skip_serializing_if = "Option::is_none")]
991 pub version: Option<String>,
992 #[serde(default, skip_serializing_if = "Option::is_none")]
994 pub skill_metadata: Option<serde_json::Value>,
995}
996
997#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
999pub struct GetSkillsReq {
1000 #[serde(flatten)]
1001 pub base: AgentCallData,
1002 pub computer: String,
1004}
1005
1006#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1010pub struct GetSkillsRet {
1011 #[serde(default)]
1013 pub skills: Vec<A2CSkillRef>,
1014 #[serde(default, skip_serializing_if = "Option::is_none")]
1016 pub req_id: Option<ReqId>,
1017}
1018
1019#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1024pub struct GetSkillReq {
1025 #[serde(flatten)]
1026 pub base: AgentCallData,
1027 pub computer: String,
1029 pub name: String,
1031 #[serde(default, skip_serializing_if = "Option::is_none")]
1033 pub rel_path: Option<String>,
1034}
1035
1036#[derive(Debug, Clone, PartialEq, Eq)]
1041pub enum SkillResource<'a> {
1042 Inline(&'a str),
1044 Blob(&'a str),
1046}
1047
1048#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
1050pub enum SkillRetError {
1051 #[error("GetSkillRet: body and blob_handle are mutually exclusive (both present)")]
1053 BothPresent,
1054 #[error("GetSkillRet: exactly one of body / blob_handle must be present (neither found)")]
1056 NeitherPresent,
1057}
1058
1059#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1065pub struct GetSkillRet {
1066 #[serde(default, skip_serializing_if = "Option::is_none")]
1068 pub name: Option<String>,
1069 #[serde(default, skip_serializing_if = "Option::is_none")]
1071 pub rel_path: Option<String>,
1072 #[serde(default, skip_serializing_if = "Option::is_none")]
1074 pub mime_type: Option<String>,
1075 #[serde(default, skip_serializing_if = "Option::is_none")]
1077 pub total_size: Option<u64>,
1078 #[serde(default, skip_serializing_if = "Option::is_none")]
1080 pub sha256: Option<String>,
1081 #[serde(default, skip_serializing_if = "Option::is_none")]
1083 pub body: Option<String>,
1084 #[serde(default, skip_serializing_if = "Option::is_none")]
1086 pub blob_handle: Option<BlobHandle>,
1087 #[serde(default, skip_serializing_if = "Option::is_none")]
1089 pub req_id: Option<ReqId>,
1090}
1091
1092impl GetSkillRet {
1093 pub fn resource(&self) -> Result<SkillResource<'_>, SkillRetError> {
1097 match (self.body.as_deref(), self.blob_handle.as_deref()) {
1098 (Some(body), None) => Ok(SkillResource::Inline(body)),
1099 (None, Some(handle)) => Ok(SkillResource::Blob(handle)),
1100 (Some(_), Some(_)) => Err(SkillRetError::BothPresent),
1101 (None, None) => Err(SkillRetError::NeitherPresent),
1102 }
1103 }
1104
1105 pub fn is_exclusive(&self) -> bool {
1107 self.body.is_some() ^ self.blob_handle.is_some()
1108 }
1109}
1110
1111pub const GET_BLOB_EVENT: &str = events::CLIENT_GET_BLOB;
1121
1122pub type BlobHandle = String;
1128
1129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1133pub struct GetBlobReq {
1134 #[serde(flatten)]
1135 pub base: AgentCallData,
1136 pub computer: String,
1138 pub blob_handle: BlobHandle,
1140 #[serde(default, skip_serializing_if = "Option::is_none")]
1142 pub chunk_offset: Option<u64>,
1143 #[serde(default, skip_serializing_if = "Option::is_none")]
1145 pub max_chunk_bytes: Option<u64>,
1146}
1147
1148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1154pub struct GetBlobRet {
1155 pub blob_handle: BlobHandle,
1157 #[serde(default, skip_serializing_if = "Option::is_none")]
1159 pub mime_type: Option<String>,
1160 pub total_size: u64,
1162 pub sha256: String,
1164 pub chunk_offset: u64,
1166 pub eof: bool,
1168 pub blob: String,
1170 #[serde(default, skip_serializing_if = "Option::is_none")]
1172 pub req_id: Option<ReqId>,
1173}
1174
1175#[cfg(test)]
1176mod tests {
1177 use super::*;
1178
1179 #[test]
1180 fn test_req_id_helpers() {
1181 let req_id = ReqId::new();
1182 assert!(!req_id.as_str().is_empty());
1183
1184 let req_id2 = ReqId::from_string("abc".to_string());
1185 assert_eq!(req_id2.as_str(), "abc");
1186
1187 let req_id3 = ReqId::default();
1188 assert!(!req_id3.as_str().is_empty());
1189 }
1190
1191 #[test]
1192 fn test_role_serde_lowercase() {
1193 let json = serde_json::to_string(&Role::Agent).unwrap();
1194 assert_eq!(json, "\"agent\"");
1195
1196 let de: Role = serde_json::from_str("\"computer\"").unwrap();
1197 assert!(matches!(de, Role::Computer));
1198 }
1199
1200 #[test]
1201 fn test_session_info_a2c_version_serde() {
1202 let with_ver = SessionInfo {
1204 sid: "sid-1".to_string(),
1205 name: "agent-1".to_string(),
1206 role: Role::Agent,
1207 office_id: "office-1".to_string(),
1208 a2c_version: Some("0.2.0".to_string()),
1209 };
1210 let json = serde_json::to_value(&with_ver).unwrap();
1211 assert_eq!(json["a2c_version"], "0.2.0");
1212
1213 let without_ver = SessionInfo {
1215 sid: "sid-2".to_string(),
1216 name: "computer-1".to_string(),
1217 role: Role::Computer,
1218 office_id: "office-1".to_string(),
1219 a2c_version: None,
1220 };
1221 let json_none = serde_json::to_value(&without_ver).unwrap();
1222 assert!(
1223 json_none.get("a2c_version").is_none(),
1224 "None 时必须省略 a2c_version 键,实得: {json_none}"
1225 );
1226
1227 let legacy = r#"{"sid":"sid-3","name":"c2","role":"computer","office_id":"office-1"}"#;
1229 let de: SessionInfo = serde_json::from_str(legacy).unwrap();
1230 assert!(de.a2c_version.is_none());
1231
1232 let back: SessionInfo = serde_json::from_value(json).unwrap();
1234 assert_eq!(back.a2c_version.as_deref(), Some("0.2.0"));
1235 }
1236
1237 #[test]
1238 fn test_notification_serde() {
1239 let n = Notification::EnterOffice(EnterOfficeNotification {
1240 office_id: "office1".to_string(),
1241 computer: Some("c1".to_string()),
1242 agent: None,
1243 });
1244
1245 let json = serde_json::to_string(&n).unwrap();
1246 let de: Notification = serde_json::from_str(&json).unwrap();
1247 match de {
1248 Notification::EnterOffice(p) => {
1249 assert_eq!(p.office_id, "office1");
1250 assert_eq!(p.computer.as_deref(), Some("c1"));
1251 assert!(p.agent.is_none());
1252 }
1253 _ => panic!("unexpected notification"),
1254 }
1255 }
1256
1257 #[test]
1258 fn test_tool_call_ret_mcp_format() {
1259 let success_ret = ToolCallRet {
1261 content: Some(vec![serde_json::json!({
1262 "type": "text",
1263 "text": "Operation completed successfully"
1264 })]),
1265 is_error: Some(false),
1266 req_id: Some(ReqId::from_string("test123".to_string())),
1267 meta: None,
1268 };
1269
1270 let json = serde_json::to_string(&success_ret).unwrap();
1271
1272 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1274 assert!(parsed.get("content").is_some());
1275 assert!(parsed.get("isError").is_some());
1276 assert_eq!(parsed.get("isError").unwrap(), false);
1277 assert_eq!(parsed.get("req_id").unwrap().as_str().unwrap(), "test123");
1278
1279 assert!(json.contains("isError"));
1281 assert!(!json.contains("is_error"));
1282 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1284 assert!(parsed.get("success").is_none());
1285 assert!(parsed.get("result").is_none());
1286 assert!(parsed.get("error").is_none());
1287 }
1288
1289 #[test]
1290 fn test_tool_call_ret_error_format() {
1291 let error_ret = ToolCallRet {
1293 content: Some(vec![serde_json::json!({
1294 "type": "text",
1295 "text": "Tool execution failed"
1296 })]),
1297 is_error: Some(true),
1298 req_id: None,
1299 meta: None,
1300 };
1301
1302 let json = serde_json::to_string(&error_ret).unwrap();
1303
1304 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1306 assert!(parsed.get("content").is_some());
1307 assert_eq!(parsed.get("isError").unwrap(), true);
1308 assert!(parsed.get("req_id").is_none());
1309
1310 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1312 assert!(parsed.get("success").is_none());
1313 assert!(parsed.get("result").is_none());
1314 assert!(parsed.get("error").is_none());
1315 }
1316
1317 #[test]
1318 fn test_tool_call_ret_minimal() {
1319 let minimal_ret = ToolCallRet {
1321 content: None,
1322 is_error: None,
1323 req_id: None,
1324 meta: None,
1325 };
1326
1327 let json = serde_json::to_string(&minimal_ret).unwrap();
1328 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1329
1330 assert_eq!(parsed, serde_json::json!({}));
1332 }
1333
1334 #[test]
1335 fn test_tool_call_ret_roundtrip() {
1336 let original = ToolCallRet {
1338 content: Some(vec![serde_json::json!({
1339 "type": "text",
1340 "text": "Test result"
1341 })]),
1342 is_error: Some(false),
1343 req_id: Some(ReqId::new()),
1344 meta: None,
1345 };
1346
1347 let json = serde_json::to_string(&original).unwrap();
1348 let deserialized: ToolCallRet = serde_json::from_str(&json).unwrap();
1349
1350 assert_eq!(original.content, deserialized.content);
1351 assert_eq!(original.is_error, deserialized.is_error);
1352 assert_eq!(original.req_id, deserialized.req_id);
1353 }
1354
1355 #[test]
1356 fn test_error_payload_flat_serialization() {
1357 let payload = ErrorPayload::new(404, "Resource not found");
1359 let v = serde_json::to_value(&payload).unwrap();
1360
1361 assert!(v.get("error").is_none(), "禁止嵌套 envelope"); assert_eq!(v.get("code").unwrap(), 404);
1363 assert_eq!(v.get("message").unwrap(), "Resource not found");
1364 assert!(v.get("details").is_none()); }
1366
1367 #[test]
1368 fn test_error_payload_with_details_serialization() {
1369 let payload = ErrorPayload::new(4014, "boom")
1371 .with_detail("mcp_server_name", "srv-a")
1372 .with_detail("hint", "retry");
1373 let v = serde_json::to_value(&payload).unwrap();
1374
1375 assert_eq!(v["code"], 4014);
1376 assert_eq!(v["details"]["mcp_server_name"], "srv-a");
1377 assert_eq!(v["details"]["hint"], "retry");
1378 }
1379
1380 #[test]
1381 fn test_is_protocol_error_payload_flat_true() {
1382 for code in [404, 4006, 4007, 4008, 4014, 4015, 4016, 4017, 4018] {
1384 let v = serde_json::json!({ "code": code, "message": "x" });
1385 assert!(
1386 is_protocol_error_payload(&v),
1387 "code {code} 应判定为协议错误负载"
1388 );
1389 }
1390 let v = serde_json::to_value(build_computer_not_found_error("c1")).unwrap();
1392 assert!(is_protocol_error_payload(&v));
1393 }
1394
1395 #[test]
1396 fn test_is_protocol_error_payload_false() {
1397 let nested = serde_json::json!({ "error": { "code": 404, "message": "x" } });
1399 assert!(!is_protocol_error_payload(&nested));
1400
1401 assert!(!is_protocol_error_payload(
1403 &serde_json::json!({ "code": 400 })
1404 ));
1405 assert!(!is_protocol_error_payload(
1406 &serde_json::json!({ "code": 9999 })
1407 ));
1408
1409 assert!(!is_protocol_error_payload(
1411 &serde_json::json!({ "message": "x" })
1412 ));
1413 assert!(!is_protocol_error_payload(
1414 &serde_json::json!({ "code": "404" })
1415 ));
1416 assert!(!is_protocol_error_payload(&serde_json::json!([
1417 "code", 404
1418 ])));
1419 assert!(!is_protocol_error_payload(&serde_json::json!("nope")));
1420 }
1421
1422 #[test]
1423 fn test_build_computer_not_found_error() {
1424 let payload = build_computer_not_found_error("my-computer");
1425 assert_eq!(payload.code, 404);
1426 assert_eq!(payload.code, i64::from(ErrorCode::NotFound.code()));
1427 assert!(payload.message.contains("my-computer"));
1428 assert_eq!(
1429 payload
1430 .details
1431 .as_ref()
1432 .unwrap()
1433 .get("computer_name")
1434 .unwrap(),
1435 "my-computer"
1436 );
1437 }
1438
1439 #[test]
1440 fn test_build_computer_not_found_error_python_byte_compat() {
1441 let v = serde_json::to_value(build_computer_not_found_error("c1")).unwrap();
1443 let expected = serde_json::json!({
1444 "code": 404,
1445 "message": "Computer with name 'c1' not found",
1446 "details": { "computer_name": "c1" }
1447 });
1448 assert_eq!(v, expected);
1449 }
1450
1451 #[test]
1452 fn test_error_payload_roundtrip() {
1453 let original = ErrorPayload::new(500, "Internal error").with_detail("trace_id", "abc123");
1455
1456 let json = serde_json::to_string(&original).unwrap();
1457 let deserialized: ErrorPayload = serde_json::from_str(&json).unwrap();
1458
1459 assert_eq!(original, deserialized);
1460 }
1461
1462 #[test]
1463 fn test_error_payload_from_error_code() {
1464 let payload = ErrorPayload::from_error_code(ErrorCode::McpServerNotFound, "boom");
1466 assert_eq!(payload.code, 4014);
1467 assert_eq!(payload.code, i64::from(ErrorCode::McpServerNotFound.code()));
1468 assert_eq!(payload.message, "boom");
1469
1470 let v = serde_json::to_value(&payload).unwrap();
1471 assert!(is_protocol_error_payload(&v));
1472 assert!(v.get("mcp_server_name").is_none());
1474 assert!(v.get("capability").is_none());
1475 assert!(v.get("details").is_none());
1476 }
1477
1478 #[test]
1479 fn test_error_payload_4014_top_level_field() {
1480 let v = serde_json::to_value(
1482 ErrorPayload::from_error_code(ErrorCode::McpServerNotFound, "not found")
1483 .with_mcp_server_name("filesystem"),
1484 )
1485 .unwrap();
1486 assert_eq!(
1487 v,
1488 serde_json::json!({
1489 "code": 4014,
1490 "message": "not found",
1491 "mcp_server_name": "filesystem"
1492 })
1493 );
1494 }
1495
1496 #[test]
1497 fn test_error_payload_4015_top_level_fields_python_shape() {
1498 let v = serde_json::to_value(
1500 ErrorPayload::from_error_code(ErrorCode::McpCapabilityNotSupported, "unsupported")
1501 .with_mcp_server_name("docs-server")
1502 .with_capability("resources"),
1503 )
1504 .unwrap();
1505 assert_eq!(
1506 v,
1507 serde_json::json!({
1508 "code": 4015,
1509 "message": "unsupported",
1510 "mcp_server_name": "docs-server",
1511 "capability": "resources"
1512 })
1513 );
1514 }
1515
1516 #[test]
1517 fn test_error_payload_captures_unknown_top_level_fields() {
1518 let wire = serde_json::json!({
1520 "code": 4014,
1521 "message": "x",
1522 "mcp_server_name": "srv", "future_string": "keep-me", "future_object": { "nested": true },
1525 "future_number": 7
1526 });
1527 let payload: ErrorPayload = serde_json::from_value(wire.clone()).unwrap();
1528
1529 assert_eq!(payload.mcp_server_name.as_deref(), Some("srv"));
1531 assert!(!payload.extra.contains_key("mcp_server_name"));
1532 assert!(!payload.extra.contains_key("code"));
1533 assert_eq!(payload.extra.get("future_string").unwrap(), "keep-me");
1535 assert_eq!(payload.extra.get("future_number").unwrap(), 7);
1536
1537 let round = serde_json::to_value(&payload).unwrap();
1539 assert_eq!(round, wire);
1540 }
1541
1542 #[test]
1543 fn test_protocol_version_constant() {
1544 assert_eq!(PROTOCOL_VERSION, "0.2.0");
1546 }
1547
1548 #[test]
1549 fn test_error_code_values() {
1550 assert_eq!(ErrorCode::NotFound.code(), 404);
1552 assert_eq!(ErrorCode::ToolAuthorizationRequired.code(), 4006);
1553 assert_eq!(ErrorCode::ToolAuthorizationFailed.code(), 4007);
1554 assert_eq!(ErrorCode::ProtocolVersionMismatch.code(), 4008);
1555 assert_eq!(ErrorCode::McpServerNotFound.code(), 4014);
1556 assert_eq!(ErrorCode::McpCapabilityNotSupported.code(), 4015);
1557 assert_eq!(ErrorCode::SkillNameInvalid.code(), 4016);
1558 assert_eq!(ErrorCode::SkillResourceNotAccessible.code(), 4017);
1559 assert_eq!(ErrorCode::BlobNotAccessible.code(), 4018);
1560 }
1561
1562 #[test]
1563 fn test_error_code_serializes_as_int() {
1564 assert_eq!(
1566 serde_json::to_string(&ErrorCode::ProtocolVersionMismatch).unwrap(),
1567 "4008"
1568 );
1569 assert_eq!(serde_json::to_string(&ErrorCode::NotFound).unwrap(), "404");
1570
1571 let v = serde_json::json!({ "code": ErrorCode::BlobNotAccessible });
1573 assert_eq!(v["code"], serde_json::json!(4018));
1574 }
1575
1576 #[test]
1577 fn test_error_code_deserializes_from_int() {
1578 let c: ErrorCode = serde_json::from_str("4014").unwrap();
1579 assert_eq!(c, ErrorCode::McpServerNotFound);
1580 assert!(serde_json::from_str::<ErrorCode>("9999").is_err());
1582 assert_eq!(ErrorCode::from_code(9999), None);
1583 }
1584
1585 #[test]
1586 fn test_error_code_int_roundtrip() {
1587 for code in [
1588 ErrorCode::NotFound,
1589 ErrorCode::ToolAuthorizationRequired,
1590 ErrorCode::ToolAuthorizationFailed,
1591 ErrorCode::ProtocolVersionMismatch,
1592 ErrorCode::McpServerNotFound,
1593 ErrorCode::McpCapabilityNotSupported,
1594 ErrorCode::SkillNameInvalid,
1595 ErrorCode::SkillResourceNotAccessible,
1596 ErrorCode::BlobNotAccessible,
1597 ] {
1598 let json = serde_json::to_string(&code).unwrap();
1599 let back: ErrorCode = serde_json::from_str(&json).unwrap();
1600 assert_eq!(code, back);
1601 assert_eq!(ErrorCode::from_code(code.code()), Some(code));
1602 }
1603 }
1604
1605 #[test]
1606 fn test_ws_close_code_distinct_from_protocol_mismatch() {
1607 assert_eq!(WS_VERSION_HANDSHAKE_REJECTED_CLOSE_CODE, 4900);
1609 assert_eq!(ErrorCode::ProtocolVersionMismatch.code(), 4008);
1610 assert_ne!(
1611 WS_VERSION_HANDSHAKE_REJECTED_CLOSE_CODE,
1612 ErrorCode::ProtocolVersionMismatch.code()
1613 );
1614 }
1615
1616 #[test]
1617 fn test_tool_lookup_vs_execution_boundary() {
1618 assert_eq!(error_codes::TOOL_NOT_FOUND, 4001);
1620 assert_eq!(error_codes::TOOL_EXECUTION_FAILED, 4003);
1621 assert_ne!(
1622 error_codes::TOOL_NOT_FOUND,
1623 error_codes::TOOL_EXECUTION_FAILED
1624 );
1625 }
1626
1627 #[test]
1630 fn test_get_blob_event_constant() {
1631 assert_eq!(GET_BLOB_EVENT, "client:get_blob");
1633 assert_eq!(GET_BLOB_EVENT, events::CLIENT_GET_BLOB);
1634 }
1635
1636 fn sample_blob_req(chunk_offset: Option<u64>, max_chunk_bytes: Option<u64>) -> GetBlobReq {
1637 GetBlobReq {
1638 base: AgentCallData {
1639 agent: "agent-1".to_string(),
1640 req_id: ReqId::from_string("r1".to_string()),
1641 },
1642 computer: "comp-1".to_string(),
1643 blob_handle: "opaque-handle".to_string(),
1644 chunk_offset,
1645 max_chunk_bytes,
1646 }
1647 }
1648
1649 #[test]
1650 fn test_get_blob_req_with_offset_serde() {
1651 let req = sample_blob_req(Some(1024), Some(65536));
1652 let v = serde_json::to_value(&req).unwrap();
1653 assert_eq!(v["agent"], "agent-1");
1655 assert_eq!(v["req_id"], "r1");
1656 assert_eq!(v["computer"], "comp-1");
1657 assert_eq!(v["blob_handle"], "opaque-handle");
1658 assert_eq!(v["chunk_offset"], 1024);
1659 assert_eq!(v["max_chunk_bytes"], 65536);
1660 let back: GetBlobReq = serde_json::from_value(v).unwrap();
1661 assert_eq!(back, req);
1662 }
1663
1664 #[test]
1665 fn test_get_blob_req_without_offset_omits_optional_keys() {
1666 let req = sample_blob_req(None, None);
1667 let v = serde_json::to_value(&req).unwrap();
1668 assert!(
1670 v.get("chunk_offset").is_none(),
1671 "缺省 chunk_offset 不应序列化"
1672 );
1673 assert!(
1674 v.get("max_chunk_bytes").is_none(),
1675 "缺省 max_chunk_bytes 不应序列化"
1676 );
1677 let back: GetBlobReq = serde_json::from_value(v).unwrap();
1679 assert_eq!(back, req);
1680 assert!(back.chunk_offset.is_none());
1681 }
1682
1683 #[test]
1684 fn test_get_blob_ret_first_and_last_chunk_roundtrip() {
1685 let first = GetBlobRet {
1687 blob_handle: "h".to_string(),
1688 mime_type: Some("application/octet-stream".to_string()),
1689 total_size: 10,
1690 sha256: "abc123".to_string(),
1691 chunk_offset: 0,
1692 eof: false,
1693 blob: "aGVsbG8=".to_string(), req_id: Some(ReqId::from_string("r1".to_string())),
1695 };
1696 let back: GetBlobRet =
1697 serde_json::from_str(&serde_json::to_string(&first).unwrap()).unwrap();
1698 assert_eq!(back, first);
1699 assert!(!back.eof);
1700
1701 let last = GetBlobRet {
1703 blob_handle: "h".to_string(),
1704 mime_type: None,
1705 total_size: 10,
1706 sha256: "abc123".to_string(),
1707 chunk_offset: 5,
1708 eof: true,
1709 blob: "d29ybGQ=".to_string(),
1710 req_id: None,
1711 };
1712 let v = serde_json::to_value(&last).unwrap();
1713 assert!(v.get("mime_type").is_none());
1714 assert!(v.get("req_id").is_none());
1715 assert_eq!(v["eof"], true);
1716 let back: GetBlobRet = serde_json::from_value(v).unwrap();
1717 assert_eq!(back, last);
1718 }
1719
1720 fn empty_ret() -> ToolCallRet {
1723 ToolCallRet {
1724 content: None,
1725 is_error: Some(true),
1726 req_id: None,
1727 meta: None,
1728 }
1729 }
1730
1731 #[test]
1732 fn test_mark_cancelled_default_and_custom_reason() {
1733 let mut ret = empty_ret();
1735 ret.mark_cancelled(None);
1736 assert!(ret.is_cancelled());
1737 assert_eq!(ret.cancel_reason(), Some("agent_requested"));
1738 let v = serde_json::to_value(&ret).unwrap();
1740 assert_eq!(v["meta"]["a2c_cancelled"], true);
1741 assert_eq!(v["meta"]["a2c_cancel_reason"], "agent_requested");
1742
1743 let mut ret2 = empty_ret();
1745 ret2.mark_cancelled(Some("operator_abort"));
1746 assert_eq!(ret2.cancel_reason(), Some("operator_abort"));
1747 }
1748
1749 #[test]
1750 fn test_mark_timeout() {
1751 let mut ret = empty_ret();
1752 assert!(!ret.is_timeout());
1753 ret.mark_timeout();
1754 assert!(ret.is_timeout());
1755 assert!(!ret.is_cancelled()); let v = serde_json::to_value(&ret).unwrap();
1757 assert_eq!(v["meta"]["a2c_timeout"], true);
1758 }
1759
1760 #[test]
1761 fn test_content_blob_sideband_placement_and_roundtrip() {
1762 let mut item = serde_json::json!({ "type": "text", "text": "" });
1764 set_content_blob_sideband(&mut item, "blob-xyz", Some(2048), Some("deadbeef"));
1765 assert_eq!(item["_meta"]["a2c_blob_handle"], "blob-xyz");
1766 assert_eq!(item["_meta"]["a2c_total_size"], 2048);
1767 assert_eq!(item["_meta"]["a2c_sha256"], "deadbeef");
1768
1769 let sb = read_content_blob_sideband(&item).expect("应读到旁路句柄");
1770 assert_eq!(sb.blob_handle, "blob-xyz");
1771 assert_eq!(sb.total_size, Some(2048));
1772 assert_eq!(sb.sha256.as_deref(), Some("deadbeef"));
1773
1774 let plain = serde_json::json!({ "type": "text", "text": "hi" });
1776 assert!(read_content_blob_sideband(&plain).is_none());
1777 }
1778
1779 #[test]
1780 fn test_result_meta_vs_child_meta_separation() {
1781 let mut item = serde_json::json!({ "type": "text", "text": "" });
1783 set_content_blob_sideband(&mut item, "h", None, None);
1784 let mut ret = ToolCallRet {
1785 content: Some(vec![item]),
1786 is_error: Some(true),
1787 req_id: None,
1788 meta: None,
1789 };
1790 ret.mark_cancelled(None);
1791
1792 let v = serde_json::to_value(&ret).unwrap();
1793 assert_eq!(v["meta"]["a2c_cancelled"], true);
1795 assert!(v["meta"].get("a2c_blob_handle").is_none());
1796 assert_eq!(v["content"][0]["_meta"]["a2c_blob_handle"], "h");
1798 assert!(v["content"][0]["_meta"].get("a2c_cancelled").is_none());
1799 }
1800
1801 #[test]
1802 fn test_tool_call_ret_deserialize_external_meta() {
1803 let cancelled_wire = r#"{"content":[{"type":"text","text":"cancelled"}],"isError":true,"meta":{"a2c_cancelled":true,"a2c_cancel_reason":"agent_requested"}}"#;
1805 let ret: ToolCallRet = serde_json::from_str(cancelled_wire).unwrap();
1806 assert!(ret.is_cancelled());
1807 assert_eq!(ret.cancel_reason(), Some("agent_requested"));
1808 assert!(!ret.is_timeout());
1809
1810 let timeout_wire = r#"{"isError":true,"meta":{"a2c_timeout":true}}"#;
1811 let ret2: ToolCallRet = serde_json::from_str(timeout_wire).unwrap();
1812 assert!(ret2.is_timeout());
1813 assert!(!ret2.is_cancelled());
1814 assert_eq!(ret2.cancel_reason(), None);
1815
1816 let plain: ToolCallRet = serde_json::from_str(r#"{"isError":false}"#).unwrap();
1818 assert!(!plain.is_cancelled() && !plain.is_timeout());
1819 }
1820
1821 #[test]
1822 fn test_agent_call_data_cancel_carrier_shape() {
1823 let cancel = AgentCallData {
1827 agent: "agent-x".to_string(),
1828 req_id: ReqId::from_string("rid_tool".to_string()),
1829 };
1830 let v = serde_json::to_value(&cancel).unwrap();
1831 assert_eq!(v["agent"], "agent-x");
1832 assert_eq!(v["req_id"], "rid_tool");
1833 assert!(
1834 v.get("computer").is_none(),
1835 "取消载体 MUST NOT 含 computer 字段,实得: {v}"
1836 );
1837 assert_eq!(
1839 v.as_object().map(|m| m.len()),
1840 Some(2),
1841 "AgentCallData 在线形态应恰为 {{agent, req_id}}"
1842 );
1843 assert_eq!(serde_json::from_value::<AgentCallData>(v).unwrap(), cancel);
1845 }
1846
1847 #[test]
1850 fn get_resources_event_const_exact() {
1851 assert_eq!(GET_RESOURCES_EVENT, "client:get_resources");
1852 assert_eq!(GET_RESOURCES_EVENT, events::CLIENT_GET_RESOURCES);
1853 }
1854
1855 #[test]
1856 fn get_resources_req_round_trip_with_and_without_cursor() {
1857 let base = AgentCallData {
1858 agent: "agent-x".to_string(),
1859 req_id: ReqId::from_string("rid-1".to_string()),
1860 };
1861 let req = GetResourcesReq {
1863 base: base.clone(),
1864 computer: "comp-1".to_string(),
1865 mcp_server: "srv-1".to_string(),
1866 cursor: None,
1867 };
1868 let v = serde_json::to_value(&req).unwrap();
1869 assert_eq!(v["agent"], "agent-x");
1870 assert_eq!(v["req_id"], "rid-1");
1871 assert_eq!(v["computer"], "comp-1");
1872 assert_eq!(v["mcp_server"], "srv-1");
1873 assert!(v.get("cursor").is_none());
1874 assert_eq!(serde_json::from_value::<GetResourcesReq>(v).unwrap(), req);
1875
1876 let req2 = GetResourcesReq {
1878 base,
1879 computer: "comp-1".to_string(),
1880 mcp_server: "srv-1".to_string(),
1881 cursor: Some("page-2".to_string()),
1882 };
1883 let v2 = serde_json::to_value(&req2).unwrap();
1884 assert_eq!(v2["cursor"], "page-2");
1885 assert_eq!(serde_json::from_value::<GetResourcesReq>(v2).unwrap(), req2);
1886 }
1887
1888 #[test]
1889 fn get_resources_ret_empty_and_populated() {
1890 let empty = GetResourcesRet::default();
1892 let v = serde_json::to_value(&empty).unwrap();
1893 assert_eq!(v["resources"], serde_json::json!([]));
1894 assert!(v.get("next_cursor").is_none());
1895
1896 let ret = GetResourcesRet {
1898 resources: vec![
1899 A2CResource {
1900 uri: Some("window://main".to_string()),
1901 name: Some("main".to_string()),
1902 annotations: Some(ResourceAnnotations {
1903 audience: Some(vec![ResourceAudience::User, ResourceAudience::Assistant]),
1904 priority: Some(0.0),
1905 last_modified: Some("2026-06-01T00:00:00Z".to_string()),
1906 }),
1907 meta: Some(serde_json::json!({ "fullscreen": true })),
1908 ..Default::default()
1909 },
1910 A2CResource {
1911 uri: Some("window://side".to_string()),
1912 annotations: Some(ResourceAnnotations {
1913 priority: Some(1.0),
1914 ..Default::default()
1915 }),
1916 ..Default::default()
1917 },
1918 ],
1919 next_cursor: Some("next".to_string()),
1920 req_id: Some(ReqId::from_string("rid-2".to_string())),
1921 };
1922 let v = serde_json::to_value(&ret).unwrap();
1923 assert_eq!(v["resources"][0]["_meta"]["fullscreen"], true);
1925 assert!(v["resources"][0].get("meta").is_none());
1926 assert_eq!(v["resources"][0]["annotations"]["audience"][0], "user");
1927 assert_eq!(v["resources"][0]["annotations"]["priority"], 0.0);
1928 assert_eq!(v["resources"][1]["annotations"]["priority"], 1.0);
1929 assert_eq!(v["next_cursor"], "next");
1930 let back: GetResourcesRet = serde_json::from_value(v).unwrap();
1931 assert_eq!(back, ret);
1932 }
1933
1934 #[test]
1937 fn skill_event_consts_exact() {
1938 assert_eq!(GET_SKILLS_EVENT, "client:get_skills");
1939 assert_eq!(GET_SKILL_EVENT, "client:get_skill");
1940 assert_eq!(UPDATE_SKILLS_EVENT, "server:update_skills");
1941 assert_eq!(UPDATE_SKILLS_NOTIFICATION, "notify:update_skills");
1942 assert_eq!(GET_SKILLS_EVENT, events::CLIENT_GET_SKILLS);
1943 assert_eq!(GET_SKILL_EVENT, events::CLIENT_GET_SKILL);
1944 assert_eq!(UPDATE_SKILLS_EVENT, events::SERVER_UPDATE_SKILLS);
1945 assert_eq!(UPDATE_SKILLS_NOTIFICATION, events::NOTIFY_UPDATE_SKILLS);
1946 }
1947
1948 #[test]
1949 fn skill_ref_three_sources_round_trip() {
1950 let user = A2CSkillRef {
1952 name: "my-helper".to_string(),
1953 source: "user".to_string(),
1954 uri: None,
1955 path: "/home/u/.skills/my-helper".to_string(),
1956 description: "a helper".to_string(),
1957 license: None,
1958 compatibility: None,
1959 allowed_tools: None,
1960 version: None,
1961 skill_metadata: None,
1962 };
1963 let v = serde_json::to_value(&user).unwrap();
1964 assert_eq!(v["name"], "my-helper");
1965 assert_eq!(v["source"], "user");
1966 assert!(v.get("uri").is_none());
1967 assert!(v.get("version").is_none());
1968 assert_eq!(serde_json::from_value::<A2CSkillRef>(v).unwrap(), user);
1969
1970 let marketplace = A2CSkillRef {
1971 name: "acme-audit:audit".to_string(),
1972 source: "marketplace:acme-skills".to_string(),
1973 ..user.clone()
1974 };
1975 assert_eq!(
1976 serde_json::from_value::<A2CSkillRef>(serde_json::to_value(&marketplace).unwrap())
1977 .unwrap(),
1978 marketplace
1979 );
1980
1981 let mcp = A2CSkillRef {
1983 name: "mcp:tfrobot-tools:code-review".to_string(),
1984 source: "mcp:tfrobot-tools".to_string(),
1985 uri: Some("skill://tfrobot-tools/code-review".to_string()),
1986 path: "/tmp/staging/code-review".to_string(),
1987 description: "review".to_string(),
1988 license: Some("MIT".to_string()),
1989 compatibility: Some(">=0.2".to_string()),
1990 allowed_tools: Some(vec!["read".to_string(), "grep".to_string()]),
1991 version: Some("1.2.0".to_string()),
1992 skill_metadata: Some(serde_json::json!({ "x": 1 })),
1993 };
1994 let v = serde_json::to_value(&mcp).unwrap();
1995 assert_eq!(v["uri"], "skill://tfrobot-tools/code-review");
1996 assert_eq!(v["allowed_tools"][1], "grep");
1997 assert_eq!(v["skill_metadata"]["x"], 1);
1998 assert_eq!(serde_json::from_value::<A2CSkillRef>(v).unwrap(), mcp);
1999 }
2000
2001 #[test]
2002 fn get_skills_req_ret_round_trip() {
2003 let req = GetSkillsReq {
2004 base: AgentCallData {
2005 agent: "a".to_string(),
2006 req_id: ReqId::from_string("r".to_string()),
2007 },
2008 computer: "c".to_string(),
2009 };
2010 let v = serde_json::to_value(&req).unwrap();
2011 assert_eq!(v["agent"], "a");
2012 assert_eq!(v["computer"], "c");
2013 assert_eq!(serde_json::from_value::<GetSkillsReq>(v).unwrap(), req);
2014
2015 let ret = GetSkillsRet::default();
2016 let v = serde_json::to_value(&ret).unwrap();
2017 assert_eq!(v["skills"], serde_json::json!([]));
2018 assert!(v.get("req_id").is_none());
2019 assert_eq!(serde_json::from_value::<GetSkillsRet>(v).unwrap(), ret);
2020 }
2021
2022 #[test]
2023 fn get_skill_req_rel_path_optional() {
2024 let base = AgentCallData {
2025 agent: "a".to_string(),
2026 req_id: ReqId::from_string("r".to_string()),
2027 };
2028 let req = GetSkillReq {
2029 base: base.clone(),
2030 computer: "c".to_string(),
2031 name: "my-helper".to_string(),
2032 rel_path: None,
2033 };
2034 let v = serde_json::to_value(&req).unwrap();
2035 assert!(v.get("rel_path").is_none());
2036 assert_eq!(serde_json::from_value::<GetSkillReq>(v).unwrap(), req);
2037
2038 let req2 = GetSkillReq {
2039 base,
2040 computer: "c".to_string(),
2041 name: "my-helper".to_string(),
2042 rel_path: Some("docs/usage.md".to_string()),
2043 };
2044 let v2 = serde_json::to_value(&req2).unwrap();
2045 assert_eq!(v2["rel_path"], "docs/usage.md");
2046 assert_eq!(serde_json::from_value::<GetSkillReq>(v2).unwrap(), req2);
2047 }
2048
2049 #[test]
2050 fn get_skill_ret_body_blob_handle_exclusive() {
2051 let inline = GetSkillRet {
2053 name: Some("my-helper".to_string()),
2054 rel_path: Some("SKILL.md".to_string()),
2055 mime_type: Some("text/markdown".to_string()),
2056 total_size: Some(42),
2057 sha256: Some("abc".to_string()),
2058 body: Some("# Hello".to_string()),
2059 ..Default::default()
2060 };
2061 assert!(inline.is_exclusive());
2062 assert_eq!(inline.resource().unwrap(), SkillResource::Inline("# Hello"));
2063 let back: GetSkillRet =
2064 serde_json::from_value(serde_json::to_value(&inline).unwrap()).unwrap();
2065 assert_eq!(back, inline);
2066
2067 let blob = GetSkillRet {
2069 name: Some("big".to_string()),
2070 blob_handle: Some("opaque-handle".to_string()),
2071 ..Default::default()
2072 };
2073 assert!(blob.is_exclusive());
2074 assert_eq!(
2075 blob.resource().unwrap(),
2076 SkillResource::Blob("opaque-handle")
2077 );
2078
2079 let both = GetSkillRet {
2081 body: Some("x".to_string()),
2082 blob_handle: Some("h".to_string()),
2083 ..Default::default()
2084 };
2085 assert!(!both.is_exclusive());
2086 assert_eq!(both.resource().unwrap_err(), SkillRetError::BothPresent);
2087
2088 let neither = GetSkillRet::default();
2090 assert!(!neither.is_exclusive());
2091 assert_eq!(
2092 neither.resource().unwrap_err(),
2093 SkillRetError::NeitherPresent
2094 );
2095 }
2096}