1use shadi_mas::{
2 AgentId, Epoch, ToolAdapter, ToolCall, ToolProvider, ToolResult,
3};
4use std::sync::Arc;
5use thiserror::Error;
6
7use crate::context::ContextPacket;
8
9#[derive(Debug, Error)]
10pub enum CliAdapterError {
11 #[error("subprocess error: {0}")]
12 Subprocess(String),
13 #[error("protocol error: {0}")]
14 Protocol(String),
15 #[error("i/o error: {0}")]
16 Io(#[from] std::io::Error),
17 #[error("serialization error: {0}")]
18 Serde(#[from] serde_json::Error),
19}
20
21pub trait CliAdapter: Send + Sync {
29 fn agent_id(&self) -> &AgentId;
31
32 fn snapshot_context(&self) -> Result<ContextPacket, CliAdapterError>;
34
35 fn inject_context(&self, ctx: &ContextPacket) -> Result<(), CliAdapterError>;
38
39 fn execute_prompt(&self, prompt: &str) -> Result<String, CliAdapterError>;
42}
43
44pub struct CliToolAdapter<A: CliAdapter> {
51 inner: Arc<A>,
52}
53
54impl<A: CliAdapter> CliToolAdapter<A> {
55 pub fn new(adapter: Arc<A>) -> Self {
56 Self { inner: adapter }
57 }
58}
59
60impl<A: CliAdapter> ToolAdapter for CliToolAdapter<A> {
61 fn provider(&self) -> ToolProvider {
62 ToolProvider::AgentSkills
63 }
64
65 fn call(&self, request: ToolCall) -> Result<ToolResult, String> {
66 let prompt =
67 String::from_utf8(request.arguments).map_err(|e| e.to_string())?;
68
69 let response = self
70 .inner
71 .execute_prompt(&prompt)
72 .map_err(|e| e.to_string())?;
73
74 Ok(ToolResult {
75 provider: ToolProvider::AgentSkills,
76 tool_name: request.tool_name,
77 payload: response.into_bytes(),
78 target: request.target,
79 correlation_id: request.correlation_id,
80 epoch: request.epoch,
81 })
82 }
83}
84
85pub fn prompt_tool_call(prompt: impl Into<String>, epoch: Epoch) -> ToolCall {
87 ToolCall {
88 provider: ToolProvider::AgentSkills,
89 tool_name: "execute_prompt".to_string(),
90 arguments: prompt.into().into_bytes(),
91 target: None,
92 correlation_id: None,
93 epoch,
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::context::ContextPacket;
101 use std::sync::Arc;
102
103 struct MockAdapter {
106 id: AgentId,
107 response: String,
108 }
109
110 impl CliAdapter for MockAdapter {
111 fn agent_id(&self) -> &AgentId {
112 &self.id
113 }
114 fn snapshot_context(&self) -> Result<ContextPacket, CliAdapterError> {
115 Ok(ContextPacket::new(self.id.0.clone()))
116 }
117 fn inject_context(&self, _: &ContextPacket) -> Result<(), CliAdapterError> {
118 Ok(())
119 }
120 fn execute_prompt(&self, _: &str) -> Result<String, CliAdapterError> {
121 Ok(self.response.clone())
122 }
123 }
124
125 struct FailingAdapter {
126 id: AgentId,
127 }
128
129 impl CliAdapter for FailingAdapter {
130 fn agent_id(&self) -> &AgentId {
131 &self.id
132 }
133 fn snapshot_context(&self) -> Result<ContextPacket, CliAdapterError> {
134 Err(CliAdapterError::Subprocess("fail".to_string()))
135 }
136 fn inject_context(&self, _: &ContextPacket) -> Result<(), CliAdapterError> {
137 Err(CliAdapterError::Subprocess("fail".to_string()))
138 }
139 fn execute_prompt(&self, _: &str) -> Result<String, CliAdapterError> {
140 Err(CliAdapterError::Subprocess("fail".to_string()))
141 }
142 }
143
144 fn make_call(args: Vec<u8>) -> ToolCall {
145 ToolCall {
146 provider: ToolProvider::AgentSkills,
147 tool_name: "test_tool".to_string(),
148 arguments: args,
149 target: None,
150 correlation_id: None,
151 epoch: Epoch(0),
152 }
153 }
154
155 #[test]
158 fn mock_adapter_all_methods_callable() {
159 let adapter = MockAdapter {
160 id: AgentId("test-id".to_string()),
161 response: "hello".to_string(),
162 };
163 assert_eq!(adapter.agent_id().0, "test-id");
164 let ctx = adapter.snapshot_context().unwrap();
165 assert_eq!(ctx.source_agent, "test-id");
166 assert!(adapter.inject_context(&ctx).is_ok());
167 }
168
169 #[test]
170 fn failing_adapter_all_methods_return_error() {
171 let adapter = FailingAdapter {
172 id: AgentId("fail-id".to_string()),
173 };
174 assert_eq!(adapter.agent_id().0, "fail-id");
175 assert!(adapter.snapshot_context().is_err());
176 let ctx = ContextPacket::new("src");
177 assert!(adapter.inject_context(&ctx).is_err());
178 }
179
180 #[test]
181 fn new_wraps_adapter_and_provider_is_agent_skills() {
182 let inner = Arc::new(MockAdapter {
183 id: AgentId("mock".to_string()),
184 response: String::new(),
185 });
186 let ta = CliToolAdapter::new(Arc::clone(&inner));
187 assert_eq!(ta.provider(), ToolProvider::AgentSkills);
188 }
189
190 #[test]
191 fn call_success_returns_response_as_payload() {
192 let inner = Arc::new(MockAdapter {
193 id: AgentId("mock".to_string()),
194 response: "fn answer() {}".to_string(),
195 });
196 let ta = CliToolAdapter::new(inner);
197 let result = ta.call(make_call(b"write a function".to_vec()));
198 assert!(result.is_ok());
199 let tr = result.unwrap();
200 assert_eq!(tr.payload, b"fn answer() {}");
201 assert_eq!(tr.provider, ToolProvider::AgentSkills);
202 assert_eq!(tr.tool_name, "test_tool");
203 assert_eq!(tr.epoch, Epoch(0));
204 assert!(tr.target.is_none());
205 assert!(tr.correlation_id.is_none());
206 }
207
208 #[test]
209 fn call_with_invalid_utf8_returns_error() {
210 let inner = Arc::new(MockAdapter {
211 id: AgentId("mock".to_string()),
212 response: String::new(),
213 });
214 let ta = CliToolAdapter::new(inner);
215 let result = ta.call(make_call(vec![0xFF, 0xFE])); assert!(result.is_err());
217 }
218
219 #[test]
220 fn call_propagates_adapter_error() {
221 let inner = Arc::new(FailingAdapter {
222 id: AgentId("fail".to_string()),
223 });
224 let ta = CliToolAdapter::new(inner);
225 let result = ta.call(make_call(b"any prompt".to_vec()));
226 assert!(result.is_err());
227 assert!(result.unwrap_err().contains("fail"));
228 }
229
230 #[test]
231 fn prompt_tool_call_sets_all_fields_correctly() {
232 let call = prompt_tool_call("implement a parser", Epoch(7));
233 assert_eq!(call.provider, ToolProvider::AgentSkills);
234 assert_eq!(call.tool_name, "execute_prompt");
235 assert_eq!(call.arguments, b"implement a parser");
236 assert_eq!(call.epoch, Epoch(7));
237 assert!(call.target.is_none());
238 assert!(call.correlation_id.is_none());
239 }
240}