test(channels): guard max_tool_iterations wiring for channel runtime (#817)
* test(channels): add regression coverage for configured tool iteration limits * chore(ci): refresh checks after first-interaction workflow fix * test(channels): reconcile merged runtime-route and iteration tests
This commit is contained in:
parent
586254a928
commit
e6029e8cec
2 changed files with 158 additions and 0 deletions
|
|
@ -16,6 +16,17 @@ Config file path:
|
||||||
| `default_model` | `anthropic/claude-sonnet-4-6` | model routed through selected provider |
|
| `default_model` | `anthropic/claude-sonnet-4-6` | model routed through selected provider |
|
||||||
| `default_temperature` | `0.7` | model temperature |
|
| `default_temperature` | `0.7` | model temperature |
|
||||||
|
|
||||||
|
## `[agent]`
|
||||||
|
|
||||||
|
| Key | Default | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `max_tool_iterations` | `10` | Maximum tool-call loop turns per user message across CLI, gateway, and channels |
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Setting `max_tool_iterations = 0` falls back to safe default `10`.
|
||||||
|
- If a channel message exceeds this value, the runtime returns: `Agent exceeded maximum tool iterations (<value>)`.
|
||||||
|
|
||||||
## `[gateway]`
|
## `[gateway]`
|
||||||
|
|
||||||
| Key | Default | Purpose |
|
| Key | Default | Purpose |
|
||||||
|
|
|
||||||
|
|
@ -2044,6 +2044,48 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct IterativeToolProvider {
|
||||||
|
required_tool_iterations: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IterativeToolProvider {
|
||||||
|
fn completed_tool_iterations(messages: &[ChatMessage]) -> usize {
|
||||||
|
messages
|
||||||
|
.iter()
|
||||||
|
.filter(|msg| msg.role == "user" && msg.content.contains("[Tool results]"))
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Provider for IterativeToolProvider {
|
||||||
|
async fn chat_with_system(
|
||||||
|
&self,
|
||||||
|
_system_prompt: Option<&str>,
|
||||||
|
_message: &str,
|
||||||
|
_model: &str,
|
||||||
|
_temperature: f64,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
Ok(tool_call_payload())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn chat_with_history(
|
||||||
|
&self,
|
||||||
|
messages: &[ChatMessage],
|
||||||
|
_model: &str,
|
||||||
|
_temperature: f64,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
let completed_iterations = Self::completed_tool_iterations(messages);
|
||||||
|
if completed_iterations >= self.required_tool_iterations {
|
||||||
|
Ok(format!(
|
||||||
|
"Completed after {completed_iterations} tool iterations."
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(tool_call_payload())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct HistoryCaptureProvider {
|
struct HistoryCaptureProvider {
|
||||||
calls: std::sync::Mutex<Vec<Vec<(String, String)>>>,
|
calls: std::sync::Mutex<Vec<Vec<(String, String)>>>,
|
||||||
|
|
@ -2401,6 +2443,111 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn process_channel_message_respects_configured_max_tool_iterations_above_default() {
|
||||||
|
let channel_impl = Arc::new(RecordingChannel::default());
|
||||||
|
let channel: Arc<dyn Channel> = channel_impl.clone();
|
||||||
|
|
||||||
|
let mut channels_by_name = HashMap::new();
|
||||||
|
channels_by_name.insert(channel.name().to_string(), channel);
|
||||||
|
|
||||||
|
let runtime_ctx = Arc::new(ChannelRuntimeContext {
|
||||||
|
channels_by_name: Arc::new(channels_by_name),
|
||||||
|
provider: Arc::new(IterativeToolProvider {
|
||||||
|
required_tool_iterations: 11,
|
||||||
|
}),
|
||||||
|
default_provider: Arc::new("test-provider".to_string()),
|
||||||
|
memory: Arc::new(NoopMemory),
|
||||||
|
tools_registry: Arc::new(vec![Box::new(MockPriceTool)]),
|
||||||
|
observer: Arc::new(NoopObserver),
|
||||||
|
system_prompt: Arc::new("test-system-prompt".to_string()),
|
||||||
|
model: Arc::new("test-model".to_string()),
|
||||||
|
temperature: 0.0,
|
||||||
|
auto_save_memory: false,
|
||||||
|
max_tool_iterations: 12,
|
||||||
|
min_relevance_score: 0.0,
|
||||||
|
conversation_histories: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
provider_cache: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
route_overrides: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
api_key: None,
|
||||||
|
api_url: None,
|
||||||
|
reliability: Arc::new(crate::config::ReliabilityConfig::default()),
|
||||||
|
provider_runtime_options: providers::ProviderRuntimeOptions::default(),
|
||||||
|
workspace_dir: Arc::new(std::env::temp_dir()),
|
||||||
|
});
|
||||||
|
|
||||||
|
process_channel_message(
|
||||||
|
runtime_ctx,
|
||||||
|
traits::ChannelMessage {
|
||||||
|
id: "msg-iter-success".to_string(),
|
||||||
|
sender: "alice".to_string(),
|
||||||
|
reply_target: "chat-iter-success".to_string(),
|
||||||
|
content: "Loop until done".to_string(),
|
||||||
|
channel: "test-channel".to_string(),
|
||||||
|
timestamp: 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let sent_messages = channel_impl.sent_messages.lock().await;
|
||||||
|
assert_eq!(sent_messages.len(), 1);
|
||||||
|
assert!(sent_messages[0].starts_with("chat-iter-success:"));
|
||||||
|
assert!(sent_messages[0].contains("Completed after 11 tool iterations."));
|
||||||
|
assert!(!sent_messages[0].contains("⚠️ Error:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn process_channel_message_reports_configured_max_tool_iterations_limit() {
|
||||||
|
let channel_impl = Arc::new(RecordingChannel::default());
|
||||||
|
let channel: Arc<dyn Channel> = channel_impl.clone();
|
||||||
|
|
||||||
|
let mut channels_by_name = HashMap::new();
|
||||||
|
channels_by_name.insert(channel.name().to_string(), channel);
|
||||||
|
|
||||||
|
let runtime_ctx = Arc::new(ChannelRuntimeContext {
|
||||||
|
channels_by_name: Arc::new(channels_by_name),
|
||||||
|
provider: Arc::new(IterativeToolProvider {
|
||||||
|
required_tool_iterations: 20,
|
||||||
|
}),
|
||||||
|
default_provider: Arc::new("test-provider".to_string()),
|
||||||
|
memory: Arc::new(NoopMemory),
|
||||||
|
tools_registry: Arc::new(vec![Box::new(MockPriceTool)]),
|
||||||
|
observer: Arc::new(NoopObserver),
|
||||||
|
system_prompt: Arc::new("test-system-prompt".to_string()),
|
||||||
|
model: Arc::new("test-model".to_string()),
|
||||||
|
temperature: 0.0,
|
||||||
|
auto_save_memory: false,
|
||||||
|
max_tool_iterations: 3,
|
||||||
|
min_relevance_score: 0.0,
|
||||||
|
conversation_histories: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
provider_cache: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
route_overrides: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
api_key: None,
|
||||||
|
api_url: None,
|
||||||
|
reliability: Arc::new(crate::config::ReliabilityConfig::default()),
|
||||||
|
provider_runtime_options: providers::ProviderRuntimeOptions::default(),
|
||||||
|
workspace_dir: Arc::new(std::env::temp_dir()),
|
||||||
|
});
|
||||||
|
|
||||||
|
process_channel_message(
|
||||||
|
runtime_ctx,
|
||||||
|
traits::ChannelMessage {
|
||||||
|
id: "msg-iter-fail".to_string(),
|
||||||
|
sender: "bob".to_string(),
|
||||||
|
reply_target: "chat-iter-fail".to_string(),
|
||||||
|
content: "Loop forever".to_string(),
|
||||||
|
channel: "test-channel".to_string(),
|
||||||
|
timestamp: 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let sent_messages = channel_impl.sent_messages.lock().await;
|
||||||
|
assert_eq!(sent_messages.len(), 1);
|
||||||
|
assert!(sent_messages[0].starts_with("chat-iter-fail:"));
|
||||||
|
assert!(sent_messages[0].contains("⚠️ Error: Agent exceeded maximum tool iterations (3)"));
|
||||||
|
}
|
||||||
|
|
||||||
struct NoopMemory;
|
struct NoopMemory;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue