Skip to main content

autumn_plugin_agent/
delegate.rs

1//! Subagents: run a whole [`Agent`] as one tool.
2//!
3//! A parent agent hands a focused task to a child agent with its own system
4//! prompt, tools, and budgets. The child's working context (every tool call
5//! and result) never enters the parent's transcript; the parent sees only
6//! the child's final answer. That keeps the parent's context small and lets
7//! you give the child tools the parent should not hold directly.
8
9use std::future::Future;
10use std::pin::Pin;
11
12use crate::agent::{Agent, AgentOutcome};
13use crate::error::{AgentError, ErrorKind};
14use crate::tools::{Tool, ToolContext, ToolEffect};
15
16/// A [`Tool`] that runs a child [`Agent`] on a task and returns its answer.
17///
18/// ```rust,no_run
19/// # use std::sync::Arc;
20/// # use autumn_plugin_agent::{Agent, LlmClient};
21/// use autumn_plugin_agent::delegate::AgentTool;
22/// use autumn_plugin_agent::tools::ToolEffect;
23///
24/// # fn demo(client: Arc<dyn LlmClient>) {
25/// let researcher = Agent::new(Arc::clone(&client))
26///     .system_prompt("You research one question and answer in five lines.")
27///     .max_steps(6);
28/// let tool = AgentTool::new(
29///     "research",
30///     "Research one question in depth and return a short answer.",
31///     researcher,
32/// )
33/// .effect(ToolEffect::ReadOnly);
34/// let parent = Agent::new(client).tool(Arc::new(tool));
35/// # }
36/// ```
37#[derive(Debug, Clone)]
38pub struct AgentTool {
39    name: String,
40    description: String,
41    agent: Agent,
42    effect: ToolEffect,
43}
44
45impl AgentTool {
46    /// Wrap `agent` as a tool. The model calls it with `{"task": "..."}`.
47    #[must_use]
48    pub fn new(name: impl Into<String>, description: impl Into<String>, agent: Agent) -> Self {
49        Self {
50            name: name.into(),
51            description: description.into(),
52            agent,
53            effect: ToolEffect::Write,
54        }
55    }
56
57    /// Declare the strongest effect among the child's tools. The default is
58    /// [`ToolEffect::Write`].
59    #[must_use]
60    pub const fn effect(mut self, effect: ToolEffect) -> Self {
61        self.effect = effect;
62        self
63    }
64}
65
66impl Tool for AgentTool {
67    fn name(&self) -> &str {
68        &self.name
69    }
70
71    fn description(&self) -> &str {
72        &self.description
73    }
74
75    fn input_schema(&self) -> serde_json::Value {
76        serde_json::json!({
77            "type": "object",
78            "properties": {
79                "task": {
80                    "type": "string",
81                    "description": "The task, with every detail the helper needs: it cannot see this conversation.",
82                }
83            },
84            "required": ["task"]
85        })
86    }
87
88    fn effect(&self) -> ToolEffect {
89        self.effect
90    }
91
92    fn execute<'a>(
93        &'a self,
94        input: serde_json::Value,
95        _ctx: &'a ToolContext,
96    ) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, AgentError>> + Send + 'a>> {
97        Box::pin(async move {
98            let task = input
99                .get("task")
100                .and_then(serde_json::Value::as_str)
101                .map(str::trim)
102                .filter(|task| !task.is_empty())
103                .ok_or_else(|| AgentError::new(ErrorKind::Tool, "give the `task` to delegate"))?;
104            let outcome = self.agent.run(task).await?;
105            let usage = outcome.usage();
106            let tokens = usage.total();
107            Ok(match outcome {
108                AgentOutcome::Completed { text, .. } => {
109                    serde_json::json!({"answer": text, "tokens": tokens})
110                }
111                AgentOutcome::AwaitingApproval { .. } => serde_json::json!({
112                    "error": "the helper needs a person to approve an action; do it another way",
113                }),
114                other => serde_json::json!({
115                    "error": "the helper stopped before it finished",
116                    "outcome": other,
117                }),
118            })
119        })
120    }
121}
122
123#[cfg(test)]
124mod tests;