Skip to main content

lc_agents/
task.rs

1//! Explicit agent task definition (P2-5)
2//!
3//! Promotes the "task" in multi-agent dispatch from a bare `String` to an
4//! explicit type [`AgentTask`]: objective, expected output, and allowed tools
5//! travel together to the child agent, replacing the bare "just give a
6//! sentence" input so dispatcher and consumer agree on the task contract.
7
8use serde::{Deserialize, Serialize};
9
10/// Child agent task (P2-5)
11///
12/// An explicit task dispatched to a child agent, carrying two layers of
13/// constraint beyond a bare `String`:
14/// - `expected_output`: the expected deliverable, so the child agent aligns its
15///   result shape;
16/// - `allowed_tools`: the child agent's tool allowlist (actual assembly is the
17///   consumer's responsibility).
18///
19/// Can degrade to a bare objective string via [`From<AgentTask>` for `String`]
20/// for `Input=String` orchestrators / executors.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct AgentTask {
23    /// Task objective: a one-sentence description of what to do.
24    pub objective: String,
25    /// Expected output (optional): the result's shape / key points, for the child agent to align to.
26    pub expected_output: Option<String>,
27    /// Allowed tool allowlist (empty = no restriction).
28    pub allowed_tools: Vec<String>,
29}
30
31impl AgentTask {
32    /// Creates a task with only an objective.
33    pub fn new(objective: impl Into<String>) -> Self {
34        Self {
35            objective: objective.into(),
36            expected_output: None,
37            allowed_tools: Vec::new(),
38        }
39    }
40
41    /// Declares the expected output.
42    pub fn with_expected_output(mut self, expected_output: impl Into<String>) -> Self {
43        self.expected_output = Some(expected_output.into());
44        self
45    }
46
47    /// Declares the allowed-tool allowlist (overwrite semantics).
48    pub fn with_allowed_tools(
49        mut self,
50        tools: impl IntoIterator<Item = impl Into<String>>,
51    ) -> Self {
52        self.allowed_tools = tools.into_iter().map(Into::into).collect();
53        self
54    }
55
56    /// Task objective.
57    pub fn objective(&self) -> &str {
58        &self.objective
59    }
60
61    /// Expected output, if any.
62    pub fn expected_output(&self) -> Option<&str> {
63        self.expected_output.as_deref()
64    }
65
66    /// Allowed-tool allowlist.
67    pub fn allowed_tools(&self) -> &[String] {
68        &self.allowed_tools
69    }
70
71    /// Whether tools are restricted (allowlist non-empty).
72    pub fn is_tool_restricted(&self) -> bool {
73        !self.allowed_tools.is_empty()
74    }
75}
76
77/// Degrades to a bare objective string: passes an `AgentTask` to orchestrators / executors that only accept `String`.
78impl From<AgentTask> for String {
79    fn from(task: AgentTask) -> Self {
80        task.objective
81    }
82}
83
84impl std::fmt::Display for AgentTask {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        write!(f, "{}", self.objective)
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_agent_task_new_has_no_constraints() {
96        let task = AgentTask::new("研究 LangChain");
97        assert_eq!(task.objective(), "研究 LangChain");
98        assert_eq!(task.expected_output(), None);
99        assert!(task.allowed_tools().is_empty());
100        assert!(!task.is_tool_restricted());
101    }
102
103    #[test]
104    fn test_agent_task_with_constraints() {
105        let task = AgentTask::new("写周报")
106            .with_expected_output("Markdown 一页")
107            .with_allowed_tools(["web_search", "calculator"]);
108        assert_eq!(task.expected_output(), Some("Markdown 一页"));
109        assert_eq!(
110            task.allowed_tools(),
111            &["web_search".to_string(), "calculator".to_string()]
112        );
113        assert!(task.is_tool_restricted());
114        assert_eq!(task.to_string(), "写周报");
115    }
116
117    #[test]
118    fn test_agent_task_from_string_loses_constraints() {
119        let task = AgentTask::new("查一下天气")
120            .with_expected_output("一句话")
121            .with_allowed_tools(["weather"]);
122        let bare: String = task.into();
123        assert_eq!(bare, "查一下天气");
124    }
125
126    #[test]
127    fn test_agent_task_serialize_roundtrip() {
128        let task = AgentTask::new("翻译")
129            .with_expected_output("中文")
130            .with_allowed_tools(["dict"]);
131        let json = serde_json::to_string(&task).unwrap();
132        let back: AgentTask = serde_json::from_str(&json).unwrap();
133        assert_eq!(back.objective(), "翻译");
134        assert_eq!(back.expected_output(), Some("中文"));
135        assert_eq!(back.allowed_tools(), &["dict".to_string()]);
136    }
137}