mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-17 14:14:26 +01:00
feat: Adds max_turns for the agent without user input (#3208)
This commit is contained in:
@@ -312,6 +312,15 @@ enum Command {
|
|||||||
)]
|
)]
|
||||||
max_tool_repetitions: Option<u32>,
|
max_tool_repetitions: Option<u32>,
|
||||||
|
|
||||||
|
/// Maximum number of turns (iterations) allowed in a single response
|
||||||
|
#[arg(
|
||||||
|
long = "max-turns",
|
||||||
|
value_name = "NUMBER",
|
||||||
|
help = "Maximum number of turns allowed without user input (default: 1000)",
|
||||||
|
long_help = "Set a limit on how many turns (iterations) the agent can take without asking for user input to continue."
|
||||||
|
)]
|
||||||
|
max_turns: Option<u32>,
|
||||||
|
|
||||||
/// Add stdio extensions with environment variables and commands
|
/// Add stdio extensions with environment variables and commands
|
||||||
#[arg(
|
#[arg(
|
||||||
long = "with-extension",
|
long = "with-extension",
|
||||||
@@ -449,6 +458,15 @@ enum Command {
|
|||||||
)]
|
)]
|
||||||
max_tool_repetitions: Option<u32>,
|
max_tool_repetitions: Option<u32>,
|
||||||
|
|
||||||
|
/// Maximum number of turns (iterations) allowed in a single response
|
||||||
|
#[arg(
|
||||||
|
long = "max-turns",
|
||||||
|
value_name = "NUMBER",
|
||||||
|
help = "Maximum number of turns allowed without user input (default: 1000)",
|
||||||
|
long_help = "Set a limit on how many turns (iterations) the agent can take without asking for user input to continue."
|
||||||
|
)]
|
||||||
|
max_turns: Option<u32>,
|
||||||
|
|
||||||
/// Identifier for this run session
|
/// Identifier for this run session
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
identifier: Option<Identifier>,
|
identifier: Option<Identifier>,
|
||||||
@@ -635,6 +653,7 @@ pub async fn cli() -> Result<()> {
|
|||||||
history,
|
history,
|
||||||
debug,
|
debug,
|
||||||
max_tool_repetitions,
|
max_tool_repetitions,
|
||||||
|
max_turns,
|
||||||
extensions,
|
extensions,
|
||||||
remote_extensions,
|
remote_extensions,
|
||||||
builtins,
|
builtins,
|
||||||
@@ -683,6 +702,7 @@ pub async fn cli() -> Result<()> {
|
|||||||
settings: None,
|
settings: None,
|
||||||
debug,
|
debug,
|
||||||
max_tool_repetitions,
|
max_tool_repetitions,
|
||||||
|
max_turns,
|
||||||
scheduled_job_id: None,
|
scheduled_job_id: None,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
@@ -731,6 +751,7 @@ pub async fn cli() -> Result<()> {
|
|||||||
no_session,
|
no_session,
|
||||||
debug,
|
debug,
|
||||||
max_tool_repetitions,
|
max_tool_repetitions,
|
||||||
|
max_turns,
|
||||||
extensions,
|
extensions,
|
||||||
remote_extensions,
|
remote_extensions,
|
||||||
builtins,
|
builtins,
|
||||||
@@ -826,6 +847,7 @@ pub async fn cli() -> Result<()> {
|
|||||||
settings: session_settings,
|
settings: session_settings,
|
||||||
debug,
|
debug,
|
||||||
max_tool_repetitions,
|
max_tool_repetitions,
|
||||||
|
max_turns,
|
||||||
scheduled_job_id,
|
scheduled_job_id,
|
||||||
interactive, // Use the interactive flag from the Run command
|
interactive, // Use the interactive flag from the Run command
|
||||||
quiet,
|
quiet,
|
||||||
@@ -950,6 +972,7 @@ pub async fn cli() -> Result<()> {
|
|||||||
settings: None::<SessionSettings>,
|
settings: None::<SessionSettings>,
|
||||||
debug: false,
|
debug: false,
|
||||||
max_tool_repetitions: None,
|
max_tool_repetitions: None,
|
||||||
|
max_turns: None,
|
||||||
scheduled_job_id: None,
|
scheduled_job_id: None,
|
||||||
interactive: true, // Default case is always interactive
|
interactive: true, // Default case is always interactive
|
||||||
quiet: false,
|
quiet: false,
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ pub async fn agent_generator(
|
|||||||
max_tool_repetitions: None,
|
max_tool_repetitions: None,
|
||||||
interactive: false, // Benchmarking is non-interactive
|
interactive: false, // Benchmarking is non-interactive
|
||||||
scheduled_job_id: None,
|
scheduled_job_id: None,
|
||||||
|
max_turns: None,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
sub_recipes: None,
|
sub_recipes: None,
|
||||||
final_output_response: None,
|
final_output_response: None,
|
||||||
|
|||||||
@@ -846,6 +846,11 @@ pub async fn configure_settings_dialog() -> Result<(), Box<dyn Error>> {
|
|||||||
"Tool Output",
|
"Tool Output",
|
||||||
"Show more or less tool output",
|
"Show more or less tool output",
|
||||||
)
|
)
|
||||||
|
.item(
|
||||||
|
"max_turns",
|
||||||
|
"Max Turns",
|
||||||
|
"Set maximum number of turns without user input",
|
||||||
|
)
|
||||||
.item(
|
.item(
|
||||||
"experiment",
|
"experiment",
|
||||||
"Toggle Experiment",
|
"Toggle Experiment",
|
||||||
@@ -876,6 +881,9 @@ pub async fn configure_settings_dialog() -> Result<(), Box<dyn Error>> {
|
|||||||
"tool_output" => {
|
"tool_output" => {
|
||||||
configure_tool_output_dialog()?;
|
configure_tool_output_dialog()?;
|
||||||
}
|
}
|
||||||
|
"max_turns" => {
|
||||||
|
configure_max_turns_dialog()?;
|
||||||
|
}
|
||||||
"experiment" => {
|
"experiment" => {
|
||||||
toggle_experiments_dialog()?;
|
toggle_experiments_dialog()?;
|
||||||
}
|
}
|
||||||
@@ -1289,3 +1297,35 @@ fn configure_scheduler_dialog() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn configure_max_turns_dialog() -> Result<(), Box<dyn Error>> {
|
||||||
|
let config = Config::global();
|
||||||
|
|
||||||
|
let current_max_turns: u32 = config.get_param("GOOSE_MAX_TURNS").unwrap_or(1000);
|
||||||
|
|
||||||
|
let max_turns_input: String =
|
||||||
|
cliclack::input("Set maximum number of agent turns without user input:")
|
||||||
|
.placeholder(¤t_max_turns.to_string())
|
||||||
|
.default_input(¤t_max_turns.to_string())
|
||||||
|
.validate(|input: &String| match input.parse::<u32>() {
|
||||||
|
Ok(value) => {
|
||||||
|
if value < 1 {
|
||||||
|
Err("Value must be at least 1")
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Err("Please enter a valid number"),
|
||||||
|
})
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
let max_turns: u32 = max_turns_input.parse()?;
|
||||||
|
config.set_param("GOOSE_MAX_TURNS", Value::from(max_turns))?;
|
||||||
|
|
||||||
|
cliclack::outro(format!(
|
||||||
|
"Set maximum turns to {} - Goose will ask for input after {} consecutive actions",
|
||||||
|
max_turns, max_turns
|
||||||
|
))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -483,6 +483,7 @@ async fn process_message_streaming(
|
|||||||
working_dir: std::env::current_dir()?,
|
working_dir: std::env::current_dir()?,
|
||||||
schedule_id: None,
|
schedule_id: None,
|
||||||
execution_mode: None,
|
execution_mode: None,
|
||||||
|
max_turns: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get response from agent
|
// Get response from agent
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ pub struct SessionBuilderConfig {
|
|||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
/// Maximum number of consecutive identical tool calls allowed
|
/// Maximum number of consecutive identical tool calls allowed
|
||||||
pub max_tool_repetitions: Option<u32>,
|
pub max_tool_repetitions: Option<u32>,
|
||||||
|
/// Maximum number of turns (iterations) allowed without user input
|
||||||
|
pub max_turns: Option<u32>,
|
||||||
/// ID of the scheduled job that triggered this session (if any)
|
/// ID of the scheduled job that triggered this session (if any)
|
||||||
pub scheduled_job_id: Option<String>,
|
pub scheduled_job_id: Option<String>,
|
||||||
/// Whether this session will be used interactively (affects debugging prompts)
|
/// Whether this session will be used interactively (affects debugging prompts)
|
||||||
@@ -122,7 +124,13 @@ async fn offer_extension_debugging_help(
|
|||||||
std::env::temp_dir().join(format!("goose_debug_extension_{}.jsonl", extension_name));
|
std::env::temp_dir().join(format!("goose_debug_extension_{}.jsonl", extension_name));
|
||||||
|
|
||||||
// Create the debugging session
|
// Create the debugging session
|
||||||
let mut debug_session = Session::new(debug_agent, Some(temp_session_file.clone()), false, None);
|
let mut debug_session = Session::new(
|
||||||
|
debug_agent,
|
||||||
|
Some(temp_session_file.clone()),
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
// Process the debugging request
|
// Process the debugging request
|
||||||
println!("{}", style("Analyzing the extension failure...").yellow());
|
println!("{}", style("Analyzing the extension failure...").yellow());
|
||||||
@@ -367,6 +375,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session {
|
|||||||
session_file.clone(),
|
session_file.clone(),
|
||||||
session_config.debug,
|
session_config.debug,
|
||||||
session_config.scheduled_job_id.clone(),
|
session_config.scheduled_job_id.clone(),
|
||||||
|
session_config.max_turns,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add extensions if provided
|
// Add extensions if provided
|
||||||
@@ -516,6 +525,7 @@ mod tests {
|
|||||||
settings: None,
|
settings: None,
|
||||||
debug: true,
|
debug: true,
|
||||||
max_tool_repetitions: Some(5),
|
max_tool_repetitions: Some(5),
|
||||||
|
max_turns: None,
|
||||||
scheduled_job_id: None,
|
scheduled_job_id: None,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
@@ -528,6 +538,7 @@ mod tests {
|
|||||||
assert_eq!(config.builtins.len(), 1);
|
assert_eq!(config.builtins.len(), 1);
|
||||||
assert!(config.debug);
|
assert!(config.debug);
|
||||||
assert_eq!(config.max_tool_repetitions, Some(5));
|
assert_eq!(config.max_tool_repetitions, Some(5));
|
||||||
|
assert!(config.max_turns.is_none());
|
||||||
assert!(config.scheduled_job_id.is_none());
|
assert!(config.scheduled_job_id.is_none());
|
||||||
assert!(config.interactive);
|
assert!(config.interactive);
|
||||||
assert!(!config.quiet);
|
assert!(!config.quiet);
|
||||||
@@ -547,6 +558,7 @@ mod tests {
|
|||||||
assert!(config.additional_system_prompt.is_none());
|
assert!(config.additional_system_prompt.is_none());
|
||||||
assert!(!config.debug);
|
assert!(!config.debug);
|
||||||
assert!(config.max_tool_repetitions.is_none());
|
assert!(config.max_tool_repetitions.is_none());
|
||||||
|
assert!(config.max_turns.is_none());
|
||||||
assert!(config.scheduled_job_id.is_none());
|
assert!(config.scheduled_job_id.is_none());
|
||||||
assert!(!config.interactive);
|
assert!(!config.interactive);
|
||||||
assert!(!config.quiet);
|
assert!(!config.quiet);
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ pub struct Session {
|
|||||||
debug: bool, // New field for debug mode
|
debug: bool, // New field for debug mode
|
||||||
run_mode: RunMode,
|
run_mode: RunMode,
|
||||||
scheduled_job_id: Option<String>, // ID of the scheduled job that triggered this session
|
scheduled_job_id: Option<String>, // ID of the scheduled job that triggered this session
|
||||||
|
max_turns: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache structure for completion data
|
// Cache structure for completion data
|
||||||
@@ -113,6 +114,7 @@ impl Session {
|
|||||||
session_file: Option<PathBuf>,
|
session_file: Option<PathBuf>,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
scheduled_job_id: Option<String>,
|
scheduled_job_id: Option<String>,
|
||||||
|
max_turns: Option<u32>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let messages = if let Some(session_file) = &session_file {
|
let messages = if let Some(session_file) = &session_file {
|
||||||
match session::read_messages(session_file) {
|
match session::read_messages(session_file) {
|
||||||
@@ -135,6 +137,7 @@ impl Session {
|
|||||||
debug,
|
debug,
|
||||||
run_mode: RunMode::Normal,
|
run_mode: RunMode::Normal,
|
||||||
scheduled_job_id,
|
scheduled_job_id,
|
||||||
|
max_turns,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,6 +760,7 @@ impl Session {
|
|||||||
.expect("failed to get current session working directory"),
|
.expect("failed to get current session working directory"),
|
||||||
schedule_id: self.scheduled_job_id.clone(),
|
schedule_id: self.scheduled_job_id.clone(),
|
||||||
execution_mode: None,
|
execution_mode: None,
|
||||||
|
max_turns: self.max_turns,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let mut stream = self
|
let mut stream = self
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ async fn handler(
|
|||||||
working_dir: PathBuf::from(session_working_dir),
|
working_dir: PathBuf::from(session_working_dir),
|
||||||
schedule_id: request.scheduled_job_id.clone(),
|
schedule_id: request.scheduled_job_id.clone(),
|
||||||
execution_mode: None,
|
execution_mode: None,
|
||||||
|
max_turns: None,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -358,6 +359,7 @@ async fn ask_handler(
|
|||||||
working_dir: PathBuf::from(session_working_dir),
|
working_dir: PathBuf::from(session_working_dir),
|
||||||
schedule_id: request.scheduled_job_id.clone(),
|
schedule_id: request.scheduled_job_id.clone(),
|
||||||
execution_mode: None,
|
execution_mode: None,
|
||||||
|
max_turns: None,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ use super::subagent_manager::SubAgentManager;
|
|||||||
use super::subagent_tools;
|
use super::subagent_tools;
|
||||||
use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE};
|
use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE};
|
||||||
|
|
||||||
|
const DEFAULT_MAX_TURNS: u32 = 1000;
|
||||||
|
|
||||||
/// The main goose Agent
|
/// The main goose Agent
|
||||||
pub struct Agent {
|
pub struct Agent {
|
||||||
pub(super) provider: Mutex<Option<Arc<dyn Provider>>>,
|
pub(super) provider: Mutex<Option<Arc<dyn Provider>>>,
|
||||||
@@ -707,7 +709,23 @@ impl Agent {
|
|||||||
|
|
||||||
Ok(Box::pin(async_stream::try_stream! {
|
Ok(Box::pin(async_stream::try_stream! {
|
||||||
let _ = reply_span.enter();
|
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 {
|
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
|
// Check for MCP notifications from subagents
|
||||||
let mcp_notifications = self.get_mcp_notifications().await;
|
let mcp_notifications = self.get_mcp_notifications().await;
|
||||||
for notification in mcp_notifications {
|
for notification in mcp_notifications {
|
||||||
|
|||||||
@@ -26,4 +26,6 @@ pub struct SessionConfig {
|
|||||||
pub schedule_id: Option<String>,
|
pub schedule_id: Option<String>,
|
||||||
/// Execution mode for scheduled jobs: "foreground" or "background"
|
/// Execution mode for scheduled jobs: "foreground" or "background"
|
||||||
pub execution_mode: Option<String>,
|
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(),
|
working_dir: current_dir.clone(),
|
||||||
schedule_id: Some(job.id.clone()),
|
schedule_id: Some(job.id.clone()),
|
||||||
execution_mode: job.execution_mode.clone(),
|
execution_mode: job.execution_mode.clone(),
|
||||||
|
max_turns: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
match agent
|
match agent
|
||||||
|
|||||||
@@ -638,3 +638,124 @@ mod final_output_tool_tests {
|
|||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -132,6 +132,18 @@ goose configure
|
|||||||
goose session --name my-session --debug
|
goose session --name my-session --debug
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Limit the maximum number of turns the agent can take before asking for user input to continue
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
|
||||||
|
**`--max-turns <NUMBER>`**
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
goose session --max-turns 50
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
### session list [options]
|
### session list [options]
|
||||||
|
|
||||||
@@ -293,6 +305,7 @@ Execute commands from an instruction file or stdin. Check out the [full guide](/
|
|||||||
- **`--debug`**: Output complete tool responses, detailed parameter values, and full file paths
|
- **`--debug`**: Output complete tool responses, detailed parameter values, and full file paths
|
||||||
- **`--explain`**: Show a recipe's title, description, and parameters
|
- **`--explain`**: Show a recipe's title, description, and parameters
|
||||||
- **`--no-session`**: Run goose commands without creating or storing a session file
|
- **`--no-session`**: Run goose commands without creating or storing a session file
|
||||||
|
- **`--max-turns <NUMBER>`**: Limit the maximum number of turns the agent can take before asking for user input to continue (default: 1000)
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
@@ -319,6 +332,9 @@ goose run --recipe recipe.yaml --explain
|
|||||||
|
|
||||||
#Run instructions from a file without session storage
|
#Run instructions from a file without session storage
|
||||||
goose run --no-session -i instructions.txt
|
goose run --no-session -i instructions.txt
|
||||||
|
|
||||||
|
#Run with a limit of 25 turns before asking for user input
|
||||||
|
goose run --max-turns 25 -i plan.md
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState, useCallback } from 'react';
|
|||||||
import { all_goose_modes, ModeSelectionItem } from './ModeSelectionItem';
|
import { all_goose_modes, ModeSelectionItem } from './ModeSelectionItem';
|
||||||
import { View, ViewOptions } from '../../../App';
|
import { View, ViewOptions } from '../../../App';
|
||||||
import { useConfig } from '../../ConfigContext';
|
import { useConfig } from '../../ConfigContext';
|
||||||
|
import { Input } from '../../ui/input';
|
||||||
|
|
||||||
interface ModeSectionProps {
|
interface ModeSectionProps {
|
||||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
setView: (view: View, viewOptions?: ViewOptions) => void;
|
||||||
@@ -9,6 +10,7 @@ interface ModeSectionProps {
|
|||||||
|
|
||||||
export const ModeSection = ({ setView }: ModeSectionProps) => {
|
export const ModeSection = ({ setView }: ModeSectionProps) => {
|
||||||
const [currentMode, setCurrentMode] = useState('auto');
|
const [currentMode, setCurrentMode] = useState('auto');
|
||||||
|
const [maxTurns, setMaxTurns] = useState<number>(1000);
|
||||||
const { read, upsert } = useConfig();
|
const { read, upsert } = useConfig();
|
||||||
|
|
||||||
const handleModeChange = async (newMode: string) => {
|
const handleModeChange = async (newMode: string) => {
|
||||||
@@ -32,9 +34,30 @@ export const ModeSection = ({ setView }: ModeSectionProps) => {
|
|||||||
}
|
}
|
||||||
}, [read]);
|
}, [read]);
|
||||||
|
|
||||||
|
const fetchMaxTurns = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const turns = (await read('GOOSE_MAX_TURNS', false)) as number;
|
||||||
|
if (turns) {
|
||||||
|
setMaxTurns(turns);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching max turns:', error);
|
||||||
|
}
|
||||||
|
}, [read]);
|
||||||
|
|
||||||
|
const handleMaxTurnsChange = async (value: number) => {
|
||||||
|
try {
|
||||||
|
await upsert('GOOSE_MAX_TURNS', value, false);
|
||||||
|
setMaxTurns(value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating max turns:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCurrentMode();
|
fetchCurrentMode();
|
||||||
}, [fetchCurrentMode]);
|
fetchMaxTurns();
|
||||||
|
}, [fetchCurrentMode, fetchMaxTurns]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="mode" className="px-8">
|
<section id="mode" className="px-8">
|
||||||
@@ -59,6 +82,24 @@ export const ModeSection = ({ setView }: ModeSectionProps) => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-6 pt-6">
|
||||||
|
<h3 className="text-textStandard mb-4">Conversation Limits</h3>
|
||||||
|
<div className="flex items-center justify-between py-2 px-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-textStandard">Max Turns</h4>
|
||||||
|
<p className="text-xs text-textSubtle mt-[2px]">
|
||||||
|
Maximum agent turns before Goose asks for user input
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={maxTurns}
|
||||||
|
onChange={(e) => handleMaxTurnsChange(Number(e.target.value))}
|
||||||
|
className="w-20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user