Skip to main content

anyclaw_sdk_agent/
lib.rs

1//! Agent adapter SDK for anyclaw.
2//!
3//! Provides the [`AgentAdapter`] trait for intercepting and transforming ACP
4//! protocol messages, and [`GenericAcpAdapter`] as a zero-cost passthrough default.
5//!
6//! # Stability
7//!
8//! This crate is **unstable** — APIs may change between releases.
9//! Enums marked `#[non_exhaustive]` will have new variants added; match arms must include `_`.
10#![warn(missing_docs)]
11
12/// ACP message adapter trait and dyn-compatible wrapper.
13pub mod adapter;
14/// Error types for the agent SDK.
15pub mod error;
16/// Zero-cost passthrough adapter implementation.
17pub mod generic;
18
19pub use adapter::{AgentAdapter, DynAgentAdapter};
20pub use error::AgentSdkError;
21pub use generic::GenericAcpAdapter;
22
23#[cfg(test)]
24mod tests {
25    use super::*;
26    use adapter::AgentAdapter;
27    use anyclaw_sdk_types::{
28        ClientCapabilities, ContentBlock, InitializeParams, InitializeResult, PermissionOption,
29        PermissionRequest, SessionNewParams, SessionNewResult, SessionPromptParams,
30        SessionUpdateEvent, SessionUpdateType, TextContent,
31    };
32
33    #[tokio::test]
34    async fn when_generic_adapter_on_initialize_result_called_then_passthrough() {
35        let adapter = GenericAcpAdapter;
36        let input = InitializeResult {
37            protocol_version: 1,
38            agent_capabilities: None,
39            defaults: None,
40            meta: None,
41        };
42        let output = AgentAdapter::on_initialize_result(&adapter, input.clone())
43            .await
44            .unwrap();
45        assert_eq!(input, output);
46    }
47
48    #[tokio::test]
49    async fn when_generic_adapter_on_session_new_result_called_then_passthrough() {
50        let adapter = GenericAcpAdapter;
51        let input = SessionNewResult {
52            session_id: "sess-42".into(),
53            meta: None,
54        };
55        let output = AgentAdapter::on_session_new_result(&adapter, input.clone())
56            .await
57            .unwrap();
58        assert_eq!(input, output);
59    }
60
61    #[test]
62    fn when_generic_adapter_cast_to_dyn_trait_object_then_compiles() {
63        let _adapter: Box<dyn DynAgentAdapter> = Box::new(GenericAcpAdapter);
64    }
65
66    #[tokio::test]
67    async fn when_generic_adapter_on_initialize_params_called_then_passthrough() {
68        let adapter = GenericAcpAdapter;
69        let input = InitializeParams {
70            protocol_version: 1,
71            capabilities: ClientCapabilities { experimental: None },
72            options: None,
73            meta: None,
74        };
75        let output = AgentAdapter::on_initialize_params(&adapter, input.clone())
76            .await
77            .unwrap();
78        assert_eq!(input, output);
79    }
80
81    #[tokio::test]
82    async fn when_generic_adapter_on_session_new_params_called_then_passthrough() {
83        let adapter = GenericAcpAdapter;
84        let input = SessionNewParams {
85            session_id: None,
86            cwd: "/tmp".into(),
87            mcp_servers: vec![],
88            meta: None,
89        };
90        let output = AgentAdapter::on_session_new_params(&adapter, input.clone())
91            .await
92            .unwrap();
93        assert_eq!(input, output);
94    }
95
96    #[tokio::test]
97    async fn when_generic_adapter_on_session_prompt_params_called_then_passthrough() {
98        let adapter = GenericAcpAdapter;
99        let input = SessionPromptParams {
100            session_id: "s1".into(),
101            prompt: vec![ContentBlock::Text(TextContent::new("hi"))],
102            meta: None,
103        };
104        let output = AgentAdapter::on_session_prompt_params(&adapter, input.clone())
105            .await
106            .unwrap();
107        assert_eq!(input, output);
108    }
109
110    #[tokio::test]
111    async fn when_generic_adapter_on_session_update_called_then_passthrough() {
112        let adapter = GenericAcpAdapter;
113        let input = SessionUpdateEvent {
114            session_id: "s1".into(),
115            update: SessionUpdateType::Result {
116                content: Some("hello".into()),
117                is_error: false,
118            },
119        };
120        let output = AgentAdapter::on_session_update(&adapter, input.clone())
121            .await
122            .unwrap();
123        assert_eq!(input, output);
124    }
125
126    #[tokio::test]
127    async fn when_generic_adapter_on_permission_request_called_then_passthrough() {
128        let adapter = GenericAcpAdapter;
129        let input = PermissionRequest {
130            request_id: "r1".into(),
131            description: "Allow?".into(),
132            options: vec![PermissionOption {
133                option_id: "allow".into(),
134                label: "Allow".into(),
135            }],
136        };
137        let output = AgentAdapter::on_permission_request(&adapter, input.clone())
138            .await
139            .unwrap();
140        assert_eq!(input, output);
141    }
142
143    #[test]
144    fn when_agent_sdk_error_checked_then_implements_std_error() {
145        let err = AgentSdkError::Protocol("test".into());
146        let _: &dyn std::error::Error = &err;
147        assert!(err.to_string().contains("test"));
148    }
149
150    #[test]
151    fn when_protocol_error_created_then_wraps_string_message() {
152        let err = AgentSdkError::Protocol("bad handshake".into());
153        assert!(matches!(err, AgentSdkError::Protocol(_)));
154        assert!(err.to_string().contains("bad handshake"));
155    }
156
157    struct InjectingAdapter;
158
159    impl AgentAdapter for InjectingAdapter {
160        async fn on_session_prompt_params(
161            &self,
162            mut params: SessionPromptParams,
163        ) -> Result<SessionPromptParams, AgentSdkError> {
164            params
165                .prompt
166                .push(ContentBlock::Text(TextContent::new("injected")));
167            Ok(params)
168        }
169    }
170
171    #[tokio::test]
172    async fn when_custom_adapter_overrides_hook_then_transformed_value_returned() {
173        let adapter = InjectingAdapter;
174        let input = SessionPromptParams {
175            session_id: "s1".into(),
176            prompt: vec![ContentBlock::Text(TextContent::new("hi"))],
177            meta: None,
178        };
179        let output = AgentAdapter::on_session_prompt_params(&adapter, input)
180            .await
181            .unwrap();
182        assert_eq!(output.prompt.len(), 2);
183        assert_eq!(output.session_id, "s1");
184    }
185
186    #[tokio::test]
187    async fn when_custom_adapter_non_overridden_hook_called_then_passthrough() {
188        let adapter = InjectingAdapter;
189        let input = InitializeParams {
190            protocol_version: 1,
191            capabilities: ClientCapabilities { experimental: None },
192            options: None,
193            meta: None,
194        };
195        let output = AgentAdapter::on_initialize_params(&adapter, input.clone())
196            .await
197            .unwrap();
198        assert_eq!(input, output);
199    }
200}