Skip to main content

clawft_plugin/
traits.rs

1//! Plugin trait definitions.
2//!
3//! Defines the 6 core plugin traits plus supporting traits:
4//! - [`Tool`] -- tool execution interface
5//! - [`ChannelAdapter`] -- channel message handling
6//! - [`PipelineStage`] -- pipeline processing stage
7//! - [`Skill`] -- skill definition with tool list
8//! - [`MemoryBackend`] -- memory storage interface
9//! - [`VoiceHandler`] -- placeholder for voice forward-compat
10//! - [`KeyValueStore`] -- key-value storage for plugins
11//! - [`ToolContext`] -- execution context for tools
12//! - [`ChannelAdapterHost`] -- host services for channel adapters
13//!
14//! All traits are `Send + Sync`. Async methods use `#[async_trait]`.
15
16use std::collections::HashMap;
17use std::sync::Arc;
18
19use async_trait::async_trait;
20
21#[cfg(feature = "native")]
22pub use tokio_util::sync::CancellationToken;
23
24#[cfg(not(feature = "native"))]
25mod cancellation {
26    use std::sync::Arc;
27    use std::sync::atomic::{AtomicBool, Ordering};
28
29    /// A lightweight cancellation token for non-native (WASM) targets.
30    ///
31    /// On native targets, this is re-exported from `tokio_util`.
32    /// On WASM, we provide a minimal `Arc<AtomicBool>` implementation
33    /// that supports `cancel()` and `is_cancelled()`.
34    #[derive(Clone)]
35    pub struct CancellationToken {
36        cancelled: Arc<AtomicBool>,
37    }
38
39    impl CancellationToken {
40        /// Create a new token that is not yet cancelled.
41        pub fn new() -> Self {
42            Self {
43                cancelled: Arc::new(AtomicBool::new(false)),
44            }
45        }
46
47        /// Signal cancellation.
48        pub fn cancel(&self) {
49            self.cancelled.store(true, Ordering::SeqCst);
50        }
51
52        /// Check whether the token has been cancelled.
53        pub fn is_cancelled(&self) -> bool {
54            self.cancelled.load(Ordering::SeqCst)
55        }
56    }
57
58    impl Default for CancellationToken {
59        fn default() -> Self {
60            Self::new()
61        }
62    }
63}
64
65#[cfg(not(feature = "native"))]
66pub use cancellation::CancellationToken;
67
68use crate::error::PluginError;
69use crate::message::MessagePayload;
70
71// ---------------------------------------------------------------------------
72// Tool
73// ---------------------------------------------------------------------------
74
75/// A tool that can be invoked by an agent or exposed via MCP.
76///
77/// Tools are the primary extension point for adding new capabilities.
78/// Each tool declares its name, description, and a JSON Schema for
79/// its parameters. The host routes `execute()` calls based on the
80/// tool name.
81#[async_trait]
82pub trait Tool: Send + Sync {
83    /// Unique tool name (e.g., `"web_search"`, `"file_read"`).
84    fn name(&self) -> &str;
85
86    /// Human-readable description of what the tool does.
87    fn description(&self) -> &str;
88
89    /// JSON Schema describing the tool's parameters.
90    ///
91    /// Returns a `serde_json::Value` representing a JSON Schema object.
92    /// The host uses this schema for validation and for MCP `tools/list`.
93    fn parameters_schema(&self) -> serde_json::Value;
94
95    /// Execute the tool with the given parameters and context.
96    ///
97    /// `params` is a JSON object matching `parameters_schema()`.
98    /// Returns a JSON value with the tool's result, or a `PluginError`.
99    async fn execute(
100        &self,
101        params: serde_json::Value,
102        ctx: &dyn ToolContext,
103    ) -> Result<serde_json::Value, PluginError>;
104}
105
106// ---------------------------------------------------------------------------
107// ChannelAdapter
108// ---------------------------------------------------------------------------
109
110/// A channel adapter for connecting to external messaging platforms.
111///
112/// Replaces the existing `Channel` trait with a plugin-oriented design
113/// that supports [`MessagePayload`] variants for text, structured, and
114/// binary (voice) content.
115#[async_trait]
116pub trait ChannelAdapter: Send + Sync {
117    /// Unique channel identifier (e.g., `"telegram"`, `"slack"`).
118    fn name(&self) -> &str;
119
120    /// Human-readable display name.
121    fn display_name(&self) -> &str;
122
123    /// Whether this adapter supports threaded conversations.
124    fn supports_threads(&self) -> bool;
125
126    /// Whether this adapter supports media/binary payloads.
127    fn supports_media(&self) -> bool;
128
129    /// Start receiving messages. Long-lived -- runs until cancelled.
130    async fn start(
131        &self,
132        host: Arc<dyn ChannelAdapterHost>,
133        cancel: CancellationToken,
134    ) -> Result<(), PluginError>;
135
136    /// Send a message payload through this channel.
137    ///
138    /// Returns a message ID on success.
139    async fn send(
140        &self,
141        target: &str,
142        payload: &MessagePayload,
143    ) -> Result<String, PluginError>;
144}
145
146/// Host services exposed to channel adapters.
147#[async_trait]
148pub trait ChannelAdapterHost: Send + Sync {
149    /// Deliver an inbound message payload to the agent pipeline.
150    async fn deliver_inbound(
151        &self,
152        channel: &str,
153        sender_id: &str,
154        chat_id: &str,
155        payload: MessagePayload,
156        metadata: HashMap<String, serde_json::Value>,
157    ) -> Result<(), PluginError>;
158}
159
160// ---------------------------------------------------------------------------
161// PipelineStage
162// ---------------------------------------------------------------------------
163
164/// Types of pipeline stages.
165#[non_exhaustive]
166#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
167#[serde(rename_all = "snake_case")]
168pub enum PipelineStageType {
169    /// Pre-processing (input validation, normalization).
170    PreProcess,
171    /// Core processing (LLM calls, tool routing).
172    Process,
173    /// Post-processing (response formatting, filtering).
174    PostProcess,
175    /// Observation / logging (read-only tap on the pipeline).
176    Observer,
177}
178
179/// A stage in the agent processing pipeline.
180///
181/// Pipeline stages are composed in order: PreProcess -> Process ->
182/// PostProcess, with Observers receiving copies at each step.
183#[async_trait]
184pub trait PipelineStage: Send + Sync {
185    /// Stage name (e.g., `"assembler"`, `"tool_router"`).
186    fn name(&self) -> &str;
187
188    /// What type of stage this is.
189    fn stage_type(&self) -> PipelineStageType;
190
191    /// Process input and return output.
192    ///
193    /// `input` is a JSON value representing the current pipeline state.
194    /// Returns the transformed pipeline state.
195    async fn process(
196        &self,
197        input: serde_json::Value,
198    ) -> Result<serde_json::Value, PluginError>;
199}
200
201// ---------------------------------------------------------------------------
202// Skill
203// ---------------------------------------------------------------------------
204
205/// A skill is a high-level agent capability composed of tools,
206/// instructions, and configuration.
207///
208/// Skills are the primary unit of agent customization. They can be
209/// loaded from SKILL.md files, bundled with plugins, or
210/// auto-generated. Skills can contribute tools that appear in MCP
211/// `tools/list` and can be invoked via slash commands.
212#[async_trait]
213pub trait Skill: Send + Sync {
214    /// Skill name (e.g., `"code-review"`, `"git-commit"`).
215    fn name(&self) -> &str;
216
217    /// Human-readable description.
218    fn description(&self) -> &str;
219
220    /// Semantic version string.
221    fn version(&self) -> &str;
222
223    /// Template variables the skill accepts (name -> description).
224    fn variables(&self) -> HashMap<String, String>;
225
226    /// Tool names this skill is allowed to invoke.
227    fn allowed_tools(&self) -> Vec<String>;
228
229    /// System instructions injected when the skill is active.
230    fn instructions(&self) -> &str;
231
232    /// Whether this skill can be invoked directly by users (e.g., via /command).
233    fn is_user_invocable(&self) -> bool;
234
235    /// Execute a tool provided by this skill.
236    ///
237    /// `tool_name` is the specific tool within this skill to call.
238    /// `params` is a JSON object of tool parameters.
239    /// `ctx` is the execution context providing key-value store access.
240    async fn execute_tool(
241        &self,
242        tool_name: &str,
243        params: serde_json::Value,
244        ctx: &dyn ToolContext,
245    ) -> Result<serde_json::Value, PluginError>;
246}
247
248// ---------------------------------------------------------------------------
249// MemoryBackend
250// ---------------------------------------------------------------------------
251
252/// A pluggable memory storage backend.
253///
254/// Supports key-value storage with optional namespace isolation,
255/// TTL, tags, and semantic search. Implementations may use
256/// in-memory stores, SQLite, HNSW indices, or external services.
257#[async_trait]
258pub trait MemoryBackend: Send + Sync {
259    /// Store a value with optional metadata.
260    async fn store(
261        &self,
262        key: &str,
263        value: &str,
264        namespace: Option<&str>,
265        ttl_seconds: Option<u64>,
266        tags: Option<Vec<String>>,
267    ) -> Result<(), PluginError>;
268
269    /// Retrieve a value by key.
270    async fn retrieve(
271        &self,
272        key: &str,
273        namespace: Option<&str>,
274    ) -> Result<Option<String>, PluginError>;
275
276    /// Search for values matching a query string.
277    ///
278    /// Returns a list of `(key, value, relevance_score)` tuples.
279    async fn search(
280        &self,
281        query: &str,
282        namespace: Option<&str>,
283        limit: Option<usize>,
284    ) -> Result<Vec<(String, String, f64)>, PluginError>;
285
286    /// Delete a value by key. Returns `true` if the key existed.
287    async fn delete(
288        &self,
289        key: &str,
290        namespace: Option<&str>,
291    ) -> Result<bool, PluginError>;
292}
293
294// ---------------------------------------------------------------------------
295// VoiceHandler (placeholder for Workstream G)
296// ---------------------------------------------------------------------------
297
298/// Placeholder trait for voice/audio processing (Workstream G).
299///
300/// This trait is defined now to reserve the capability type and
301/// ensure forward-compatibility. Implementations will be added
302/// when Workstream G begins. The `voice` feature flag gates
303/// any voice-specific dependencies.
304#[async_trait]
305pub trait VoiceHandler: Send + Sync {
306    /// Process raw audio input and return a transcription or response.
307    ///
308    /// `audio_data` is raw audio bytes. `mime_type` indicates the format
309    /// (e.g., `"audio/wav"`, `"audio/opus"`).
310    async fn process_audio(
311        &self,
312        audio_data: &[u8],
313        mime_type: &str,
314    ) -> Result<String, PluginError>;
315
316    /// Synthesize text into audio output.
317    ///
318    /// Returns audio bytes and the MIME type of the output format.
319    async fn synthesize(
320        &self,
321        text: &str,
322    ) -> Result<(Vec<u8>, String), PluginError>;
323}
324
325// ---------------------------------------------------------------------------
326// KeyValueStore
327// ---------------------------------------------------------------------------
328
329/// Key-value store interface exposed to plugins via [`ToolContext`].
330///
331/// This is the cross-element contract defined in the integration spec.
332/// Implementations may be backed by in-memory maps, SQLite, or the
333/// agent's memory system.
334#[async_trait]
335pub trait KeyValueStore: Send + Sync {
336    /// Get a value by key. Returns `None` if not found.
337    async fn get(&self, key: &str) -> Result<Option<String>, PluginError>;
338
339    /// Set a value for a key.
340    async fn set(&self, key: &str, value: &str) -> Result<(), PluginError>;
341
342    /// Delete a key. Returns `true` if the key existed.
343    async fn delete(&self, key: &str) -> Result<bool, PluginError>;
344
345    /// List all keys with an optional prefix filter.
346    async fn list_keys(&self, prefix: Option<&str>) -> Result<Vec<String>, PluginError>;
347}
348
349// ---------------------------------------------------------------------------
350// ToolContext
351// ---------------------------------------------------------------------------
352
353/// Execution context passed to [`Tool::execute()`] and [`Skill::execute_tool()`].
354///
355/// Provides access to the key-value store, plugin identity, and
356/// agent identity. This is the plugin's window into the host.
357pub trait ToolContext: Send + Sync {
358    /// Access the key-value store for plugin state.
359    fn key_value_store(&self) -> &dyn KeyValueStore;
360
361    /// The ID of the plugin that owns this tool.
362    fn plugin_id(&self) -> &str;
363
364    /// The ID of the agent invoking this tool.
365    fn agent_id(&self) -> &str;
366}
367
368// ---------------------------------------------------------------------------
369// Tests
370// ---------------------------------------------------------------------------
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    /// Compile-time assertion that a type is Send + Sync.
377    fn assert_send_sync<T: Send + Sync + ?Sized>() {}
378
379    #[test]
380    fn test_traits_are_send_sync() {
381        // All 6 plugin traits as trait objects
382        assert_send_sync::<dyn Tool>();
383        assert_send_sync::<dyn ChannelAdapter>();
384        assert_send_sync::<dyn PipelineStage>();
385        assert_send_sync::<dyn Skill>();
386        assert_send_sync::<dyn MemoryBackend>();
387        assert_send_sync::<dyn VoiceHandler>();
388
389        // Supporting traits
390        assert_send_sync::<dyn KeyValueStore>();
391        assert_send_sync::<dyn ToolContext>();
392        assert_send_sync::<dyn ChannelAdapterHost>();
393    }
394
395    #[test]
396    fn test_pipeline_stage_type_serde_roundtrip() {
397        let types = vec![
398            PipelineStageType::PreProcess,
399            PipelineStageType::Process,
400            PipelineStageType::PostProcess,
401            PipelineStageType::Observer,
402        ];
403        for t in &types {
404            let json = serde_json::to_string(t).unwrap();
405            let restored: PipelineStageType = serde_json::from_str(&json).unwrap();
406            assert_eq!(&restored, t);
407        }
408    }
409
410    #[test]
411    fn test_pipeline_stage_type_json_values() {
412        assert_eq!(
413            serde_json::to_string(&PipelineStageType::PreProcess).unwrap(),
414            "\"pre_process\""
415        );
416        assert_eq!(
417            serde_json::to_string(&PipelineStageType::Process).unwrap(),
418            "\"process\""
419        );
420        assert_eq!(
421            serde_json::to_string(&PipelineStageType::PostProcess).unwrap(),
422            "\"post_process\""
423        );
424        assert_eq!(
425            serde_json::to_string(&PipelineStageType::Observer).unwrap(),
426            "\"observer\""
427        );
428    }
429
430    // -----------------------------------------------------------------------
431    // Mock implementations to verify trait usability
432    // -----------------------------------------------------------------------
433
434    struct MockKvStore;
435
436    #[async_trait]
437    impl KeyValueStore for MockKvStore {
438        async fn get(&self, _key: &str) -> Result<Option<String>, PluginError> {
439            Ok(None)
440        }
441        async fn set(&self, _key: &str, _value: &str) -> Result<(), PluginError> {
442            Ok(())
443        }
444        async fn delete(&self, _key: &str) -> Result<bool, PluginError> {
445            Ok(false)
446        }
447        async fn list_keys(&self, _prefix: Option<&str>) -> Result<Vec<String>, PluginError> {
448            Ok(vec![])
449        }
450    }
451
452    struct MockToolContext;
453
454    impl ToolContext for MockToolContext {
455        fn key_value_store(&self) -> &dyn KeyValueStore {
456            &MockKvStore
457        }
458        fn plugin_id(&self) -> &str {
459            "mock-plugin"
460        }
461        fn agent_id(&self) -> &str {
462            "mock-agent"
463        }
464    }
465
466    struct MockTool;
467
468    #[async_trait]
469    impl Tool for MockTool {
470        fn name(&self) -> &str {
471            "mock_tool"
472        }
473        fn description(&self) -> &str {
474            "A mock tool for testing"
475        }
476        fn parameters_schema(&self) -> serde_json::Value {
477            serde_json::json!({
478                "type": "object",
479                "properties": {
480                    "input": { "type": "string" }
481                }
482            })
483        }
484        async fn execute(
485            &self,
486            params: serde_json::Value,
487            _ctx: &dyn ToolContext,
488        ) -> Result<serde_json::Value, PluginError> {
489            Ok(serde_json::json!({
490                "result": format!("processed: {}", params)
491            }))
492        }
493    }
494
495    struct MockChannelAdapter;
496
497    #[async_trait]
498    impl ChannelAdapter for MockChannelAdapter {
499        fn name(&self) -> &str {
500            "mock"
501        }
502        fn display_name(&self) -> &str {
503            "Mock Channel"
504        }
505        fn supports_threads(&self) -> bool {
506            false
507        }
508        fn supports_media(&self) -> bool {
509            true
510        }
511        async fn start(
512            &self,
513            _host: Arc<dyn ChannelAdapterHost>,
514            cancel: CancellationToken,
515        ) -> Result<(), PluginError> {
516            cancel.cancelled().await;
517            Ok(())
518        }
519        async fn send(
520            &self,
521            _target: &str,
522            _payload: &MessagePayload,
523        ) -> Result<String, PluginError> {
524            Ok("msg-001".into())
525        }
526    }
527
528    struct MockPipelineStage;
529
530    #[async_trait]
531    impl PipelineStage for MockPipelineStage {
532        fn name(&self) -> &str {
533            "mock_stage"
534        }
535        fn stage_type(&self) -> PipelineStageType {
536            PipelineStageType::PreProcess
537        }
538        async fn process(
539            &self,
540            input: serde_json::Value,
541        ) -> Result<serde_json::Value, PluginError> {
542            Ok(input)
543        }
544    }
545
546    struct MockSkill;
547
548    #[async_trait]
549    impl Skill for MockSkill {
550        fn name(&self) -> &str {
551            "mock-skill"
552        }
553        fn description(&self) -> &str {
554            "A mock skill"
555        }
556        fn version(&self) -> &str {
557            "1.0.0"
558        }
559        fn variables(&self) -> HashMap<String, String> {
560            HashMap::new()
561        }
562        fn allowed_tools(&self) -> Vec<String> {
563            vec!["mock_tool".into()]
564        }
565        fn instructions(&self) -> &str {
566            "Do mock things."
567        }
568        fn is_user_invocable(&self) -> bool {
569            true
570        }
571        async fn execute_tool(
572            &self,
573            tool_name: &str,
574            _params: serde_json::Value,
575            _ctx: &dyn ToolContext,
576        ) -> Result<serde_json::Value, PluginError> {
577            Ok(serde_json::json!({ "tool": tool_name, "status": "ok" }))
578        }
579    }
580
581    struct MockMemoryBackend;
582
583    #[async_trait]
584    impl MemoryBackend for MockMemoryBackend {
585        async fn store(
586            &self,
587            _key: &str,
588            _value: &str,
589            _namespace: Option<&str>,
590            _ttl_seconds: Option<u64>,
591            _tags: Option<Vec<String>>,
592        ) -> Result<(), PluginError> {
593            Ok(())
594        }
595        async fn retrieve(
596            &self,
597            _key: &str,
598            _namespace: Option<&str>,
599        ) -> Result<Option<String>, PluginError> {
600            Ok(Some("stored-value".into()))
601        }
602        async fn search(
603            &self,
604            _query: &str,
605            _namespace: Option<&str>,
606            _limit: Option<usize>,
607        ) -> Result<Vec<(String, String, f64)>, PluginError> {
608            Ok(vec![("key".into(), "value".into(), 0.95)])
609        }
610        async fn delete(
611            &self,
612            _key: &str,
613            _namespace: Option<&str>,
614        ) -> Result<bool, PluginError> {
615            Ok(true)
616        }
617    }
618
619    struct MockVoiceHandler;
620
621    #[async_trait]
622    impl VoiceHandler for MockVoiceHandler {
623        async fn process_audio(
624            &self,
625            _audio_data: &[u8],
626            _mime_type: &str,
627        ) -> Result<String, PluginError> {
628            Ok("transcribed text".into())
629        }
630        async fn synthesize(
631            &self,
632            _text: &str,
633        ) -> Result<(Vec<u8>, String), PluginError> {
634            Ok((vec![0u8; 100], "audio/wav".into()))
635        }
636    }
637
638    #[tokio::test]
639    async fn test_tool_trait_implementation() {
640        let tool = MockTool;
641        let ctx = MockToolContext;
642        assert_eq!(tool.name(), "mock_tool");
643        assert_eq!(tool.description(), "A mock tool for testing");
644        assert!(tool.parameters_schema().is_object());
645        let result = tool
646            .execute(serde_json::json!({"input": "test"}), &ctx)
647            .await
648            .unwrap();
649        assert!(result["result"].as_str().unwrap().contains("test"));
650    }
651
652    #[tokio::test]
653    async fn test_channel_adapter_trait_implementation() {
654        let adapter = MockChannelAdapter;
655        assert_eq!(adapter.name(), "mock");
656        assert_eq!(adapter.display_name(), "Mock Channel");
657        assert!(!adapter.supports_threads());
658        assert!(adapter.supports_media());
659        let payload = MessagePayload::text("hello");
660        let msg_id = adapter.send("target", &payload).await.unwrap();
661        assert_eq!(msg_id, "msg-001");
662    }
663
664    #[tokio::test]
665    async fn test_pipeline_stage_trait_implementation() {
666        let stage = MockPipelineStage;
667        assert_eq!(stage.name(), "mock_stage");
668        assert_eq!(stage.stage_type(), PipelineStageType::PreProcess);
669        let input = serde_json::json!({"data": "test"});
670        let output = stage.process(input.clone()).await.unwrap();
671        assert_eq!(output, input);
672    }
673
674    #[tokio::test]
675    async fn test_skill_trait_implementation() {
676        let skill = MockSkill;
677        let ctx = MockToolContext;
678        assert_eq!(skill.name(), "mock-skill");
679        assert_eq!(skill.description(), "A mock skill");
680        assert_eq!(skill.version(), "1.0.0");
681        assert!(skill.variables().is_empty());
682        assert_eq!(skill.allowed_tools(), vec!["mock_tool"]);
683        assert_eq!(skill.instructions(), "Do mock things.");
684        assert!(skill.is_user_invocable());
685        let result = skill
686            .execute_tool("mock_tool", serde_json::json!({}), &ctx)
687            .await
688            .unwrap();
689        assert_eq!(result["tool"], "mock_tool");
690        assert_eq!(result["status"], "ok");
691    }
692
693    #[tokio::test]
694    async fn test_memory_backend_trait_implementation() {
695        let backend = MockMemoryBackend;
696        backend
697            .store("key", "value", None, None, None)
698            .await
699            .unwrap();
700        let val = backend.retrieve("key", None).await.unwrap();
701        assert_eq!(val, Some("stored-value".into()));
702        let results = backend.search("query", None, Some(10)).await.unwrap();
703        assert_eq!(results.len(), 1);
704        assert_eq!(results[0].0, "key");
705        let deleted = backend.delete("key", None).await.unwrap();
706        assert!(deleted);
707    }
708
709    #[tokio::test]
710    async fn test_voice_handler_trait_implementation() {
711        let handler = MockVoiceHandler;
712        let text = handler
713            .process_audio(&[0u8; 100], "audio/wav")
714            .await
715            .unwrap();
716        assert_eq!(text, "transcribed text");
717        let (audio, mime) = handler.synthesize("hello").await.unwrap();
718        assert!(!audio.is_empty());
719        assert_eq!(mime, "audio/wav");
720    }
721
722    #[tokio::test]
723    async fn test_key_value_store_trait_implementation() {
724        let store = MockKvStore;
725        let val = store.get("missing").await.unwrap();
726        assert!(val.is_none());
727        store.set("key", "value").await.unwrap();
728        let deleted = store.delete("key").await.unwrap();
729        assert!(!deleted); // Mock always returns false
730        let keys = store.list_keys(None).await.unwrap();
731        assert!(keys.is_empty());
732    }
733
734    #[test]
735    fn test_tool_context_trait_implementation() {
736        let ctx = MockToolContext;
737        assert_eq!(ctx.plugin_id(), "mock-plugin");
738        assert_eq!(ctx.agent_id(), "mock-agent");
739        // key_value_store() returns a reference -- just verify it compiles
740        let _kv = ctx.key_value_store();
741    }
742
743    #[test]
744    fn test_trait_objects_can_be_boxed() {
745        // Verify all trait objects can be put behind Box<dyn Trait>
746        let _tool: Box<dyn Tool> = Box::new(MockTool);
747        let _channel: Box<dyn ChannelAdapter> = Box::new(MockChannelAdapter);
748        let _stage: Box<dyn PipelineStage> = Box::new(MockPipelineStage);
749        let _skill: Box<dyn Skill> = Box::new(MockSkill);
750        let _memory: Box<dyn MemoryBackend> = Box::new(MockMemoryBackend);
751        let _voice: Box<dyn VoiceHandler> = Box::new(MockVoiceHandler);
752        let _kv: Box<dyn KeyValueStore> = Box::new(MockKvStore);
753        let _ctx: Box<dyn ToolContext> = Box::new(MockToolContext);
754    }
755
756    #[test]
757    fn test_trait_objects_can_be_arced() {
758        // Verify all trait objects can be put behind Arc<dyn Trait>
759        let _tool: Arc<dyn Tool> = Arc::new(MockTool);
760        let _channel: Arc<dyn ChannelAdapter> = Arc::new(MockChannelAdapter);
761        let _stage: Arc<dyn PipelineStage> = Arc::new(MockPipelineStage);
762        let _skill: Arc<dyn Skill> = Arc::new(MockSkill);
763        let _memory: Arc<dyn MemoryBackend> = Arc::new(MockMemoryBackend);
764        let _voice: Arc<dyn VoiceHandler> = Arc::new(MockVoiceHandler);
765        let _kv: Arc<dyn KeyValueStore> = Arc::new(MockKvStore);
766    }
767}