diff --git a/crates/goose/src/gateway/handler.rs b/crates/goose/src/gateway/handler.rs index 0e682fece1..b7060c5ee0 100644 --- a/crates/goose/src/gateway/handler.rs +++ b/crates/goose/src/gateway/handler.rs @@ -18,6 +18,25 @@ use crate::session::{EnabledExtensionsState, ExtensionState, Session}; use super::pairing::PairingStore; use super::{Gateway, GatewayConfig, IncomingMessage, OutgoingMessage, PairingState, PlatformUser}; +/// Conservative default cap on tool-calling loops for gateway sessions. +/// +/// Chat platforms like Telegram favor short, snappy replies, so the gateway +/// keeps a stricter default than the global `GOOSE_MAX_TURNS` ceiling. Users +/// can override this through `GOOSE_GATEWAY_MAX_TURNS` (gateway-specific) or +/// `GOOSE_MAX_TURNS` (applies globally). +const DEFAULT_GATEWAY_MAX_TURNS: u32 = 5; + +/// Resolve the max turns to use for a gateway session. +/// +/// Precedence: `GOOSE_GATEWAY_MAX_TURNS` -> `GOOSE_MAX_TURNS` -> +/// `DEFAULT_GATEWAY_MAX_TURNS`. Extracted as a pure function so the +/// precedence rules can be unit-tested without touching the global config. +fn resolve_gateway_max_turns(gateway_override: Option, global_max_turns: Option) -> u32 { + gateway_override + .or(global_max_turns) + .unwrap_or(DEFAULT_GATEWAY_MAX_TURNS) +} + #[derive(Clone)] pub struct GatewayHandler { agent_manager: Arc, @@ -316,12 +335,20 @@ impl GatewayHandler { // dozens of tool calls before responding. After this many // LLM→tool round-trips the agent will stop and reply with // whatever it has. - const GATEWAY_MAX_TURNS: u32 = 5; + // + // Honors `GOOSE_GATEWAY_MAX_TURNS` (gateway-specific override) and + // `GOOSE_MAX_TURNS` (global), falling back to a conservative default + // so the limit is configurable without editing the source. + let config = Config::global(); + let max_turns = resolve_gateway_max_turns( + config.get_param::("GOOSE_GATEWAY_MAX_TURNS").ok(), + config.get_param::("GOOSE_MAX_TURNS").ok(), + ); let session_config = SessionConfig { id: session_id.to_string(), schedule_id: None, - max_turns: Some(GATEWAY_MAX_TURNS), + max_turns: Some(max_turns), retry_config: None, }; @@ -509,3 +536,31 @@ fn gateway_working_dir(platform: &str, user_id: &str) -> PathBuf { .join(platform) .join(user_id) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn defaults_when_no_overrides() { + assert_eq!( + resolve_gateway_max_turns(None, None), + DEFAULT_GATEWAY_MAX_TURNS + ); + } + + #[test] + fn uses_global_max_turns_when_gateway_unset() { + assert_eq!(resolve_gateway_max_turns(None, Some(42)), 42); + } + + #[test] + fn gateway_override_wins_over_global() { + assert_eq!(resolve_gateway_max_turns(Some(10), Some(42)), 10); + } + + #[test] + fn gateway_override_used_when_global_unset() { + assert_eq!(resolve_gateway_max_turns(Some(25), None), 25); + } +} diff --git a/documentation/docs/guides/environment-variables.md b/documentation/docs/guides/environment-variables.md index 3c3fa3f97d..b423ece7c1 100644 --- a/documentation/docs/guides/environment-variables.md +++ b/documentation/docs/guides/environment-variables.md @@ -232,6 +232,7 @@ These variables control how goose manages conversation sessions and context. |----------|---------|---------|---------| | `GOOSE_CONTEXT_STRATEGY` | Controls how goose handles context limit exceeded situations | "summarize", "truncate", "clear", "prompt" | "prompt" (interactive), "summarize" (headless) | | `GOOSE_MAX_TURNS` | [Maximum number of turns](/docs/guides/sessions/smart-context-management#maximum-turns) allowed without user input | Integer (e.g., 10, 50, 100) | 1000 | +| `GOOSE_GATEWAY_MAX_TURNS` | Maximum number of turns for gateway sessions (e.g., Telegram). Overrides `GOOSE_MAX_TURNS` for gateway traffic only, so chat platforms can keep a stricter cap than CLI/desktop sessions. | Integer (e.g., 5, 10, 25) | Falls back to `GOOSE_MAX_TURNS`, then 5 | | `GOOSE_SUBAGENT_MAX_TURNS` | Sets the maximum turns allowed for a [subagent](/docs/guides/context-engineering/subagents) to complete before timeout. Can be overridden by [`settings.max_turns`](/docs/guides/recipes/recipe-reference#settings) in recipes or subagent tool calls. | Integer (e.g., 25) | 25 | | `GOOSE_MAX_BACKGROUND_TASKS` | Sets the maximum number of concurrent background [subagent](/docs/guides/context-engineering/subagents) tasks goose can run at once | Integer (e.g., 1, 5, 10) | 5 | | `CONTEXT_FILE_NAMES` | Specifies custom filenames for [hint/context files](/docs/guides/context-engineering/using-goosehints#custom-context-files) | JSON array of strings (e.g., `["CLAUDE.md", ".goosehints"]`) | `[".goosehints"]` | @@ -268,6 +269,10 @@ export GOOSE_MAX_TURNS=25 # Set a reasonable limit for production export GOOSE_MAX_TURNS=100 +# Raise the per-gateway cap without changing CLI/desktop limits +# (applies to Telegram and other gateway sessions only) +export GOOSE_GATEWAY_MAX_TURNS=15 + # Customize the default subagent turn limit # Note: This can be overridden per-recipe or per-subagent using the max_turns setting export GOOSE_SUBAGENT_MAX_TURNS=50