Skip to main content

sgr_agent/
agent.rs

1//! Agent trait — decides what to do next given conversation history and tools.
2
3use crate::types::{SgrError, ToolCall};
4
5/// Agent's decision: what to do next.
6#[derive(Debug, Clone)]
7pub struct Decision {
8    /// Agent's assessment of the current situation.
9    pub situation: String,
10    /// Task breakdown (reasoning steps).
11    pub task: Vec<String>,
12    /// Tool calls to execute.
13    pub tool_calls: Vec<ToolCall>,
14    /// If true, the agent considers the task done.
15    pub completed: bool,
16}
17
18/// Errors from agent operations.
19#[derive(Debug, thiserror::Error)]
20pub enum AgentError {
21    #[error("LLM error: {0}")]
22    Llm(#[from] SgrError),
23    #[error("tool error: {0}")]
24    Tool(String),
25    #[error("loop detected after {0} iterations")]
26    LoopDetected(usize),
27    #[error("max steps reached: {0}")]
28    MaxSteps(usize),
29    #[error("cancelled")]
30    Cancelled,
31}
32
33/// An agent that decides what tools to call given conversation history.
34///
35/// Lifecycle hooks (all have default no-op implementations):
36/// - `prepare_context` — called before each step to modify context
37/// - `prepare_tools` — called before each step to filter/modify tool set
38/// - `after_action` — called after tool execution with results
39#[async_trait::async_trait]
40pub trait Agent: Send + Sync {
41    /// Given messages and available tools, decide what to do next.
42    async fn decide(
43        &self,
44        messages: &[crate::types::Message],
45        tools: &crate::registry::ToolRegistry,
46    ) -> Result<Decision, AgentError>;
47
48    /// Hook: modify context before each step. Default: no-op.
49    fn prepare_context(
50        &self,
51        _ctx: &mut crate::context::AgentContext,
52        _messages: &[crate::types::Message],
53    ) {
54    }
55
56    /// Hook: filter or reorder tools before each step.
57    /// Returns tool names to include. Default: all tools.
58    fn prepare_tools(
59        &self,
60        _ctx: &crate::context::AgentContext,
61        tools: &crate::registry::ToolRegistry,
62    ) -> Vec<String> {
63        tools.list().iter().map(|t| t.name().to_string()).collect()
64    }
65
66    /// Hook: called after tool execution with the tool name and output.
67    /// Can modify context or messages. Default: no-op.
68    fn after_action(
69        &self,
70        _ctx: &mut crate::context::AgentContext,
71        _tool_name: &str,
72        _output: &str,
73    ) {
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn decision_completed() {
83        let d = Decision {
84            situation: "done".into(),
85            task: vec![],
86            tool_calls: vec![],
87            completed: true,
88        };
89        assert!(d.completed);
90    }
91
92    #[test]
93    fn agent_error_display() {
94        let err = AgentError::LoopDetected(5);
95        assert_eq!(err.to_string(), "loop detected after 5 iterations");
96        let err = AgentError::MaxSteps(50);
97        assert_eq!(err.to_string(), "max steps reached: 50");
98    }
99}