Skip to main content

traitclaw_core/traits/
output_transformer.rs

1//! Async tool output transformation — the v0.3.0 evolution of [`OutputProcessor`].
2//!
3//! [`OutputTransformer`] is the async replacement for the sync [`OutputProcessor`] trait.
4//! It adds context-awareness (tool name and agent state) and supports async operations
5//! such as LLM-powered summarization of tool output.
6//!
7//! A blanket implementation is provided so that any existing [`OutputProcessor`]
8//! implementation automatically works as an [`OutputTransformer`] with zero code changes.
9//!
10//! # Example
11//!
12//! ```rust
13//! use traitclaw_core::traits::output_transformer::OutputTransformer;
14//! use traitclaw_core::types::agent_state::AgentState;
15//! use async_trait::async_trait;
16//!
17//! struct BudgetAwareTransformer {
18//!     max_chars: usize,
19//! }
20//!
21//! #[async_trait]
22//! impl OutputTransformer for BudgetAwareTransformer {
23//!     async fn transform(
24//!         &self,
25//!         output: String,
26//!         tool_name: &str,
27//!         state: &AgentState,
28//!     ) -> String {
29//!         // Truncate more aggressively when context is nearly full
30//!         let budget = if state.context_utilization() > 0.8 {
31//!             self.max_chars / 2
32//!         } else {
33//!             self.max_chars
34//!         };
35//!         if output.len() > budget {
36//!             format!("{}...\n[truncated]", &output[..budget])
37//!         } else {
38//!             output
39//!         }
40//!     }
41//! }
42//! ```
43
44use async_trait::async_trait;
45
46#[allow(deprecated)]
47use crate::traits::output_processor::OutputProcessor;
48use crate::types::agent_state::AgentState;
49
50/// Async trait for context-aware tool output transformation.
51///
52/// Called after each tool execution to process the output before adding it
53/// to the message context. Supports async operations such as LLM-powered
54/// summarization.
55///
56/// # Migration from `OutputProcessor`
57///
58/// `OutputTransformer` replaces the sync [`OutputProcessor`] trait.
59/// Existing `OutputProcessor` implementations work automatically via a blanket impl.
60/// The blanket impl ignores the `tool_name` and `state` parameters.
61#[async_trait]
62pub trait OutputTransformer: Send + Sync {
63    /// Transform tool output, optionally using context about which tool
64    /// produced it and the current agent state.
65    ///
66    /// `tool_name` identifies the tool that produced the output.
67    /// `state` provides runtime context (token usage, iteration count, etc.).
68    async fn transform(&self, output: String, tool_name: &str, state: &AgentState) -> String;
69
70    /// Estimate token count for a given output string.
71    ///
72    /// Default: 4-characters ≈ 1-token approximation.
73    fn estimate_output_tokens(&self, output: &str) -> usize {
74        output.len() / 4 + 1
75    }
76}
77
78// ---------------------------------------------------------------------------
79// Blanket impl: any OutputProcessor automatically becomes an OutputTransformer
80// ---------------------------------------------------------------------------
81
82#[allow(deprecated)]
83#[async_trait]
84impl<T: OutputProcessor + 'static> OutputTransformer for T {
85    async fn transform(&self, output: String, _tool_name: &str, _state: &AgentState) -> String {
86        // Delegate to the sync OutputProcessor::process, ignoring context
87        OutputProcessor::process(self, output)
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use crate::types::model_info::ModelTier;
95    use std::sync::Arc;
96
97    // ── Object safety: confirm Arc<dyn OutputTransformer> compiles ───────
98    #[test]
99    fn test_output_transformer_is_object_safe() {
100        struct Dummy;
101
102        #[async_trait]
103        impl OutputTransformer for Dummy {
104            async fn transform(
105                &self,
106                output: String,
107                _tool_name: &str,
108                _state: &AgentState,
109            ) -> String {
110                output
111            }
112        }
113
114        let _: Arc<dyn OutputTransformer> = Arc::new(Dummy);
115    }
116
117    // ── Blanket impl: OutputProcessor → OutputTransformer ────────────────
118    #[tokio::test]
119    async fn test_blanket_impl_delegates_to_output_processor() {
120        #[allow(deprecated)]
121        use crate::traits::output_processor::TruncateProcessor;
122
123        let processor = TruncateProcessor::new(10);
124        let state = AgentState::new(ModelTier::Small, 4096);
125
126        // Short input — should pass through
127        let result =
128            OutputTransformer::transform(&processor, "hello".to_string(), "test_tool", &state)
129                .await;
130        assert_eq!(result, "hello");
131
132        // Long input — should be truncated
133        let long = "12345678901234567890".to_string();
134        let result = OutputTransformer::transform(&processor, long, "test_tool", &state).await;
135        assert!(
136            result.contains("[output truncated]"),
137            "blanket impl should delegate to sync process: {result}"
138        );
139    }
140
141    // ── Default estimate_output_tokens() ────────────────────────────────
142    #[test]
143    fn test_default_estimate_output_tokens() {
144        struct Dummy;
145
146        #[async_trait]
147        impl OutputTransformer for Dummy {
148            async fn transform(
149                &self,
150                output: String,
151                _tool_name: &str,
152                _state: &AgentState,
153            ) -> String {
154                output
155            }
156        }
157
158        let t = Dummy;
159        // 400 chars → 400/4 + 1 = 101 tokens
160        assert_eq!(t.estimate_output_tokens(&"a".repeat(400)), 101);
161        // empty → 0/4 + 1 = 1 token
162        assert_eq!(t.estimate_output_tokens(""), 1);
163    }
164
165    // ── Context-aware transformer test ──────────────────────────────────
166    #[tokio::test]
167    async fn test_context_aware_transformer() {
168        struct ToolAwareTransformer;
169
170        #[async_trait]
171        impl OutputTransformer for ToolAwareTransformer {
172            async fn transform(
173                &self,
174                output: String,
175                tool_name: &str,
176                state: &AgentState,
177            ) -> String {
178                format!(
179                    "[tool={}, util={:.0}%] {}",
180                    tool_name,
181                    state.context_utilization() * 100.0,
182                    output
183                )
184            }
185        }
186
187        let t = ToolAwareTransformer;
188        let mut state = AgentState::new(ModelTier::Medium, 1000);
189        state.total_context_tokens = 750;
190
191        let result = t.transform("data".to_string(), "search", &state).await;
192        assert!(result.contains("tool=search"), "should include tool name");
193        assert!(result.contains("75%"), "should include utilization");
194        assert!(result.contains("data"), "should include original output");
195    }
196}