Skip to main content

agentlib_reasoning/
autonomous.rs

1use crate::utils::extract_text;
2use agentlib_core::{
3    ModelMessage, ModelRequest, ReasoningContext, ReasoningEngine, ReasoningStep, Role,
4    ToolDefinition,
5};
6use anyhow::{Result, anyhow};
7use async_trait::async_trait;
8
9pub struct AutonomousEngine {
10    max_steps: usize,
11    finish_tool_name: String,
12    finish_tool_description: String,
13}
14
15impl AutonomousEngine {
16    pub fn new(
17        max_steps: usize,
18        finish_tool_name: Option<String>,
19        finish_tool_description: Option<String>,
20    ) -> Self {
21        Self {
22            max_steps,
23            finish_tool_name: finish_tool_name.unwrap_or_else(|| "finish".to_string()),
24            finish_tool_description: finish_tool_description.unwrap_or_else(|| {
25                "Signal that you have completed the task. Call this with your final answer."
26                    .to_string()
27            }),
28        }
29    }
30}
31
32impl Default for AutonomousEngine {
33    fn default() -> Self {
34        Self::new(30, None, None)
35    }
36}
37
38#[async_trait]
39impl ReasoningEngine for AutonomousEngine {
40    fn name(&self) -> &str {
41        "autonomous"
42    }
43
44    async fn execute(&self, r_ctx: &mut ReasoningContext<'_>) -> Result<String> {
45        let finish_schema = ToolDefinition {
46            name: self.finish_tool_name.clone(),
47            description: Some(self.finish_tool_description.clone()),
48            parameters: serde_json::json!({
49                "type": "object",
50                "properties": {
51                    "result": {
52                        "type": "string",
53                        "description": "Your final answer or output."
54                    }
55                },
56                "required": ["result"]
57            }),
58        };
59
60        let mut all_schemas = r_ctx.tools.list();
61        all_schemas.push(finish_schema);
62
63        let mut steps = 0;
64
65        while steps < self.max_steps {
66            let request = ModelRequest {
67                messages: r_ctx.ctx.messages.clone(),
68                tools: Some(all_schemas.clone()),
69            };
70
71            let response = r_ctx.model.complete(request).await?;
72
73            // Accumulate usage
74            if let Some(usage) = &response.usage {
75                r_ctx.ctx.usage.prompt_tokens += usage.prompt_tokens;
76                r_ctx.ctx.usage.completion_tokens += usage.completion_tokens;
77                r_ctx.ctx.usage.total_tokens += usage.total_tokens;
78            }
79
80            r_ctx.ctx.messages.push(response.message.clone());
81
82            if !response.message.content.is_empty() {
83                r_ctx.push_step(ReasoningStep::Thought {
84                    content: response.message.content.clone(),
85                    engine: self.name().to_string(),
86                });
87            }
88
89            if let Some(tool_calls) = &response.message.tool_calls {
90                if tool_calls.is_empty() {
91                    let answer = extract_text(&response.message.content);
92                    r_ctx.push_step(ReasoningStep::Response {
93                        content: answer.clone(),
94                        engine: self.name().to_string(),
95                    });
96                    return Ok(answer);
97                }
98
99                let finish_call = tool_calls
100                    .iter()
101                    .find(|tc| tc.name == self.finish_tool_name);
102
103                if let Some(tc) = finish_call {
104                    let result = tc
105                        .arguments
106                        .get("result")
107                        .and_then(|v| v.as_str())
108                        .map(|s| s.to_string())
109                        .unwrap_or_else(|| response.message.content.clone());
110
111                    r_ctx.ctx.messages.push(ModelMessage {
112                        role: Role::Tool,
113                        content: serde_json::json!({ "done": true }).to_string(),
114                        tool_call_id: Some(tc.id.clone()),
115                        tool_calls: None,
116                    });
117
118                    r_ctx.push_step(ReasoningStep::Response {
119                        content: result.clone(),
120                        engine: self.name().to_string(),
121                    });
122                    return Ok(result);
123                }
124
125                for tc in tool_calls {
126                    r_ctx
127                        .call_tool(&tc.name, tc.arguments.clone(), tc.id.clone())
128                        .await?;
129                }
130            } else {
131                let answer = extract_text(&response.message.content);
132                r_ctx.push_step(ReasoningStep::Response {
133                    content: answer.clone(),
134                    engine: self.name().to_string(),
135                });
136                return Ok(answer);
137            }
138
139            steps += 1;
140        }
141
142        Err(anyhow!(
143            "[AutonomousEngine] Max steps ({}) reached. The agent did not call \"{}\".",
144            self.max_steps,
145            self.finish_tool_name
146        ))
147    }
148}