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