Skip to main content

oxi_agent/tools/
yield_tool.rs

1/// Yield tool — subagent result submission.
2///
3/// Subagents call yield to submit their output with type labels and optional
4/// structured data. Supports final results, incremental findings, and progress
5/// updates. In omp this is a HIDDEN_TOOL — available but not listed in the
6/// BUILTIN_TOOLS map.
7use super::{AgentTool, AgentToolResult, ToolContext, ToolError, ToolExecutionMode, ToolTier};
8use async_trait::async_trait;
9use serde_json::{Value, json};
10use std::sync::LazyLock;
11use std::sync::Mutex;
12use tokio::sync::oneshot;
13
14/// A yielded result.
15#[derive(Clone, Debug)]
16#[allow(dead_code)]
17struct YieldedResult {
18    /// Submission type: "result" (final), "findings" (incremental), "progress".
19    result_type: String,
20    /// Structured output data.
21    data: Option<Value>,
22    /// Error message (if failed).
23    error: Option<String>,
24    /// Summary text.
25    summary: Option<String>,
26    /// Timestamp.
27    timestamp: u64,
28}
29
30/// Global yield store.
31static YIELD_STORE: LazyLock<Mutex<Vec<YieldedResult>>> = LazyLock::new(|| Mutex::new(Vec::new()));
32
33/// YieldTool — submit subagent output.
34pub struct YieldTool;
35
36#[async_trait]
37impl AgentTool for YieldTool {
38    fn name(&self) -> &str {
39        "yield"
40    }
41
42    fn label(&self) -> &str {
43        "Yield"
44    }
45
46    fn description(&self) -> &str {
47        concat!(
48            "Submit subagent output with type labels and optional structured data. ",
49            "Use 'result' for final output, 'findings' for incremental code review ",
50            "findings, or 'progress' for interim updates. ",
51            "Provide 'data' for structured results or 'error' for failures."
52        )
53    }
54
55    fn essential(&self) -> bool {
56        false
57    }
58
59    fn parameters_schema(&self) -> Value {
60        json!({
61            "type": "object",
62            "properties": {
63                "type": {
64                    "type": "string",
65                    "enum": ["result", "findings", "progress"],
66                    "description": "Submission type: 'result' (final), 'findings' (incremental), 'progress' (interim)."
67                },
68                "data": {
69                    "type": "object",
70                    "description": "Structured output data (arbitrary JSON)."
71                },
72                "error": {
73                    "type": "string",
74                    "description": "Error message if the subagent failed."
75                },
76                "summary": {
77                    "type": "string",
78                    "description": "One-line summary of the result."
79                }
80            }
81        })
82    }
83
84    fn intent(&self) -> Option<&str> {
85        Some("Submit subagent output")
86    }
87
88    fn execution_mode(&self) -> ToolExecutionMode {
89        ToolExecutionMode::SequentialOnly
90    }
91
92    fn tool_tier(&self) -> ToolTier {
93        ToolTier::Read
94    }
95
96    async fn execute(
97        &self,
98        _tool_call_id: &str,
99        params: Value,
100        _signal: Option<oneshot::Receiver<()>>,
101        _ctx: &ToolContext,
102    ) -> Result<AgentToolResult, ToolError> {
103        let result_type = params
104            .get("type")
105            .and_then(|v| v.as_str())
106            .unwrap_or("result")
107            .to_string();
108
109        let data = params.get("data").cloned();
110        let error = params
111            .get("error")
112            .and_then(|v| v.as_str())
113            .map(String::from);
114        let summary = params
115            .get("summary")
116            .and_then(|v| v.as_str())
117            .map(String::from);
118
119        let yielded = YieldedResult {
120            result_type: result_type.clone(),
121            data,
122            error: error.clone(),
123            summary: summary.clone(),
124            timestamp: std::time::SystemTime::now()
125                .duration_since(std::time::UNIX_EPOCH)
126                .map(|d| d.as_secs())
127                .unwrap_or(0),
128        };
129
130        let mut store = YIELD_STORE
131            .lock()
132            .map_err(|e| format!("Yield store lock error: {}", e))?;
133        store.push(yielded);
134
135        let mut lines = vec![format!("Yielded result (type: {})", result_type)];
136        if let Some(ref s) = summary {
137            lines.push(format!("Summary: {}", s));
138        }
139        if let Some(ref e) = error {
140            lines.push(format!("Error: {}", e));
141        }
142
143        Ok(AgentToolResult::success(format!(
144            "{}\nResult stored {} entry.",
145            lines.join("\n"),
146            store.len()
147        )))
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[tokio::test]
156    async fn test_yield_result() {
157        // Reset
158        *YIELD_STORE.lock().unwrap() = Vec::new();
159
160        let tool = YieldTool;
161        let params = json!({
162            "type": "result",
163            "data": {"output": "completed analysis"},
164            "summary": "Analysis complete"
165        });
166        let result = tool
167            .execute("id", params, None, &ToolContext::default())
168            .await
169            .unwrap();
170        assert!(result.success);
171        assert!(result.output.contains("Yielded result"));
172    }
173
174    #[tokio::test]
175    async fn test_yield_findings() {
176        let tool = YieldTool;
177        let params = json!({
178            "type": "findings",
179            "data": {
180                "findings": [
181                    {"title": "Security issue", "priority": "P0", "file": "src/auth.rs"}
182                ]
183            }
184        });
185        let result = tool
186            .execute("id", params, None, &ToolContext::default())
187            .await
188            .unwrap();
189        assert!(result.success);
190    }
191
192    #[tokio::test]
193    async fn test_yield_error() {
194        let tool = YieldTool;
195        let params = json!({
196            "type": "result",
197            "error": "Analysis failed: timeout",
198            "summary": "Failed to complete"
199        });
200        let result = tool
201            .execute("id", params, None, &ToolContext::default())
202            .await
203            .unwrap();
204        assert!(result.success);
205        assert!(result.output.contains("Error:"));
206    }
207}