feat: Adds max_turns for the agent without user input (#3208)
This commit is contained in:
@@ -55,6 +55,8 @@ use super::subagent_manager::SubAgentManager;
|
||||
use super::subagent_tools;
|
||||
use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE};
|
||||
|
||||
const DEFAULT_MAX_TURNS: u32 = 1000;
|
||||
|
||||
/// The main goose Agent
|
||||
pub struct Agent {
|
||||
pub(super) provider: Mutex<Option<Arc<dyn Provider>>>,
|
||||
@@ -707,7 +709,23 @@ impl Agent {
|
||||
|
||||
Ok(Box::pin(async_stream::try_stream! {
|
||||
let _ = reply_span.enter();
|
||||
let mut turns_taken = 0u32;
|
||||
let max_turns = session
|
||||
.as_ref()
|
||||
.and_then(|s| s.max_turns)
|
||||
.unwrap_or_else(|| {
|
||||
config.get_param("GOOSE_MAX_TURNS").unwrap_or(DEFAULT_MAX_TURNS)
|
||||
});
|
||||
|
||||
loop {
|
||||
turns_taken += 1;
|
||||
if turns_taken > max_turns {
|
||||
yield AgentEvent::Message(Message::assistant().with_text(
|
||||
"I've reached the maximum number of actions I can do without user input. Would you like me to continue?"
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for MCP notifications from subagents
|
||||
let mcp_notifications = self.get_mcp_notifications().await;
|
||||
for notification in mcp_notifications {
|
||||
|
||||
@@ -26,4 +26,6 @@ pub struct SessionConfig {
|
||||
pub schedule_id: Option<String>,
|
||||
/// Execution mode for scheduled jobs: "foreground" or "background"
|
||||
pub execution_mode: Option<String>,
|
||||
/// Maximum number of turns (iterations) allowed without user input
|
||||
pub max_turns: Option<u32>,
|
||||
}
|
||||
|
||||
@@ -1203,6 +1203,7 @@ async fn run_scheduled_job_internal(
|
||||
working_dir: current_dir.clone(),
|
||||
schedule_id: Some(job.id.clone()),
|
||||
execution_mode: job.execution_mode.clone(),
|
||||
max_turns: None,
|
||||
};
|
||||
|
||||
match agent
|
||||
|
||||
@@ -638,3 +638,124 @@ mod final_output_tool_tests {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod max_turns_tests {
|
||||
use super::*;
|
||||
use async_trait::async_trait;
|
||||
use goose::message::MessageContent;
|
||||
use goose::model::ModelConfig;
|
||||
use goose::providers::base::{Provider, ProviderMetadata, ProviderUsage, Usage};
|
||||
use goose::providers::errors::ProviderError;
|
||||
use goose::session::storage::Identifier;
|
||||
use mcp_core::tool::{Tool, ToolCall};
|
||||
use std::path::PathBuf;
|
||||
|
||||
struct MockToolProvider {}
|
||||
|
||||
impl MockToolProvider {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Provider for MockToolProvider {
|
||||
async fn complete(
|
||||
&self,
|
||||
_system_prompt: &str,
|
||||
_messages: &[Message],
|
||||
_tools: &[Tool],
|
||||
) -> Result<(Message, ProviderUsage), ProviderError> {
|
||||
let tool_call = ToolCall::new("test_tool", serde_json::json!({"param": "value"}));
|
||||
let message = Message::assistant().with_tool_request("call_123", Ok(tool_call));
|
||||
|
||||
let usage = ProviderUsage::new(
|
||||
"mock-model".to_string(),
|
||||
Usage::new(Some(10), Some(5), Some(15)),
|
||||
);
|
||||
|
||||
Ok((message, usage))
|
||||
}
|
||||
|
||||
fn get_model_config(&self) -> ModelConfig {
|
||||
ModelConfig::new("mock-model".to_string())
|
||||
}
|
||||
|
||||
fn metadata() -> ProviderMetadata {
|
||||
ProviderMetadata {
|
||||
name: "mock".to_string(),
|
||||
display_name: "Mock Provider".to_string(),
|
||||
description: "Mock provider for testing".to_string(),
|
||||
default_model: "mock-model".to_string(),
|
||||
known_models: vec![],
|
||||
model_doc_link: "".to_string(),
|
||||
config_keys: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_max_turns_limit() -> Result<()> {
|
||||
let agent = Agent::new();
|
||||
let provider = Arc::new(MockToolProvider::new());
|
||||
agent.update_provider(provider).await?;
|
||||
// The mock provider will call a non-existent tool, which will fail and allow the loop to continue
|
||||
|
||||
// Create session config with max_turns = 1
|
||||
let session_config = goose::agents::SessionConfig {
|
||||
id: Identifier::Name("test_session".to_string()),
|
||||
working_dir: PathBuf::from("/tmp"),
|
||||
schedule_id: None,
|
||||
execution_mode: None,
|
||||
max_turns: Some(1),
|
||||
};
|
||||
let messages = vec![Message::user().with_text("Hello")];
|
||||
|
||||
let reply_stream = agent.reply(&messages, Some(session_config)).await?;
|
||||
tokio::pin!(reply_stream);
|
||||
|
||||
let mut responses = Vec::new();
|
||||
while let Some(response_result) = reply_stream.next().await {
|
||||
match response_result {
|
||||
Ok(AgentEvent::Message(response)) => {
|
||||
if let Some(MessageContent::ToolConfirmationRequest(ref req)) =
|
||||
response.content.first()
|
||||
{
|
||||
agent.handle_confirmation(
|
||||
req.id.clone(),
|
||||
goose::permission::PermissionConfirmation {
|
||||
principal_type: goose::permission::permission_confirmation::PrincipalType::Tool,
|
||||
permission: goose::permission::Permission::AllowOnce,
|
||||
}
|
||||
).await;
|
||||
}
|
||||
responses.push(response);
|
||||
}
|
||||
Ok(AgentEvent::McpNotification(_)) => {}
|
||||
Ok(AgentEvent::ModelChange { .. }) => {}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
responses.len() >= 1,
|
||||
"Expected at least 1 response, got {}",
|
||||
responses.len()
|
||||
);
|
||||
|
||||
// Look for the max turns message as the last response
|
||||
let last_response = responses.last().unwrap();
|
||||
let last_content = last_response.content.first().unwrap();
|
||||
if let MessageContent::Text(text_content) = last_content {
|
||||
assert!(text_content.text.contains(
|
||||
"I've reached the maximum number of actions I can do without user input"
|
||||
));
|
||||
} else {
|
||||
panic!("Expected text content in last message");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user