Compare commits
1 Commits
master
...
feat/confi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
194c2ab718 |
@ -183,6 +183,8 @@ Delegate sub-agent configurations. Each key under `[agents]` defines a named sub
|
|||||||
| `agentic` | `false` | Enable multi-turn tool-call loop mode for the sub-agent |
|
| `agentic` | `false` | Enable multi-turn tool-call loop mode for the sub-agent |
|
||||||
| `allowed_tools` | `[]` | Tool allowlist for agentic mode |
|
| `allowed_tools` | `[]` | Tool allowlist for agentic mode |
|
||||||
| `max_iterations` | `10` | Max tool-call iterations for agentic mode |
|
| `max_iterations` | `10` | Max tool-call iterations for agentic mode |
|
||||||
|
| `timeout_secs` | `120` | Timeout in seconds for non-agentic provider calls (1–3600) |
|
||||||
|
| `agentic_timeout_secs` | `300` | Timeout in seconds for agentic sub-agent loops (1–3600) |
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
@ -199,11 +201,13 @@ max_depth = 2
|
|||||||
agentic = true
|
agentic = true
|
||||||
allowed_tools = ["web_search", "http_request", "file_read"]
|
allowed_tools = ["web_search", "http_request", "file_read"]
|
||||||
max_iterations = 8
|
max_iterations = 8
|
||||||
|
agentic_timeout_secs = 600
|
||||||
|
|
||||||
[agents.coder]
|
[agents.coder]
|
||||||
provider = "ollama"
|
provider = "ollama"
|
||||||
model = "qwen2.5-coder:32b"
|
model = "qwen2.5-coder:32b"
|
||||||
temperature = 0.2
|
temperature = 0.2
|
||||||
|
timeout_secs = 60
|
||||||
```
|
```
|
||||||
|
|
||||||
## `[runtime]`
|
## `[runtime]`
|
||||||
|
|||||||
@ -449,6 +449,14 @@ pub struct DelegateAgentConfig {
|
|||||||
/// Maximum tool-call iterations in agentic mode.
|
/// Maximum tool-call iterations in agentic mode.
|
||||||
#[serde(default = "default_max_tool_iterations")]
|
#[serde(default = "default_max_tool_iterations")]
|
||||||
pub max_iterations: usize,
|
pub max_iterations: usize,
|
||||||
|
/// Timeout in seconds for non-agentic provider calls.
|
||||||
|
/// Defaults to 120 when unset. Must be between 1 and 3600.
|
||||||
|
#[serde(default)]
|
||||||
|
pub timeout_secs: Option<u64>,
|
||||||
|
/// Timeout in seconds for agentic sub-agent loops.
|
||||||
|
/// Defaults to 300 when unset. Must be between 1 and 3600.
|
||||||
|
#[serde(default)]
|
||||||
|
pub agentic_timeout_secs: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Swarms ──────────────────────────────────────────────────────
|
// ── Swarms ──────────────────────────────────────────────────────
|
||||||
@ -7252,6 +7260,31 @@ impl Config {
|
|||||||
anyhow::bail!("security.nevis: {msg}");
|
anyhow::bail!("security.nevis: {msg}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delegate agent timeouts
|
||||||
|
const MAX_DELEGATE_TIMEOUT_SECS: u64 = 3600;
|
||||||
|
for (name, agent) in &self.agents {
|
||||||
|
if let Some(timeout) = agent.timeout_secs {
|
||||||
|
if timeout == 0 {
|
||||||
|
anyhow::bail!("agents.{name}.timeout_secs must be greater than 0");
|
||||||
|
}
|
||||||
|
if timeout > MAX_DELEGATE_TIMEOUT_SECS {
|
||||||
|
anyhow::bail!(
|
||||||
|
"agents.{name}.timeout_secs exceeds max {MAX_DELEGATE_TIMEOUT_SECS}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(timeout) = agent.agentic_timeout_secs {
|
||||||
|
if timeout == 0 {
|
||||||
|
anyhow::bail!("agents.{name}.agentic_timeout_secs must be greater than 0");
|
||||||
|
}
|
||||||
|
if timeout > MAX_DELEGATE_TIMEOUT_SECS {
|
||||||
|
anyhow::bail!(
|
||||||
|
"agents.{name}.agentic_timeout_secs exceeds max {MAX_DELEGATE_TIMEOUT_SECS}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Transcription
|
// Transcription
|
||||||
{
|
{
|
||||||
let dp = self.transcription.default_provider.trim();
|
let dp = self.transcription.default_provider.trim();
|
||||||
@ -8818,6 +8851,8 @@ tool_dispatcher = "xml"
|
|||||||
agentic: false,
|
agentic: false,
|
||||||
allowed_tools: Vec::new(),
|
allowed_tools: Vec::new(),
|
||||||
max_iterations: 10,
|
max_iterations: 10,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1281,6 +1281,8 @@ mod tests {
|
|||||||
agentic: false,
|
agentic: false,
|
||||||
allowed_tools: Vec::new(),
|
allowed_tools: Vec::new(),
|
||||||
max_iterations: 10,
|
max_iterations: 10,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
config.agents.insert(
|
config.agents.insert(
|
||||||
@ -1295,6 +1297,8 @@ mod tests {
|
|||||||
agentic: false,
|
agentic: false,
|
||||||
allowed_tools: Vec::new(),
|
allowed_tools: Vec::new(),
|
||||||
max_iterations: 10,
|
max_iterations: 10,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -296,8 +296,9 @@ impl Tool for DelegateTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wrap the provider call in a timeout to prevent indefinite blocking
|
// Wrap the provider call in a timeout to prevent indefinite blocking
|
||||||
|
let timeout_secs = agent_config.timeout_secs.unwrap_or(DELEGATE_TIMEOUT_SECS);
|
||||||
let result = tokio::time::timeout(
|
let result = tokio::time::timeout(
|
||||||
Duration::from_secs(DELEGATE_TIMEOUT_SECS),
|
Duration::from_secs(timeout_secs),
|
||||||
provider.chat_with_system(
|
provider.chat_with_system(
|
||||||
agent_config.system_prompt.as_deref(),
|
agent_config.system_prompt.as_deref(),
|
||||||
&full_prompt,
|
&full_prompt,
|
||||||
@ -314,7 +315,7 @@ impl Tool for DelegateTool {
|
|||||||
success: false,
|
success: false,
|
||||||
output: String::new(),
|
output: String::new(),
|
||||||
error: Some(format!(
|
error: Some(format!(
|
||||||
"Agent '{agent_name}' timed out after {DELEGATE_TIMEOUT_SECS}s"
|
"Agent '{agent_name}' timed out after {timeout_secs}s"
|
||||||
)),
|
)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -401,8 +402,11 @@ impl DelegateTool {
|
|||||||
|
|
||||||
let noop_observer = NoopObserver;
|
let noop_observer = NoopObserver;
|
||||||
|
|
||||||
|
let agentic_timeout_secs = agent_config
|
||||||
|
.agentic_timeout_secs
|
||||||
|
.unwrap_or(DELEGATE_AGENTIC_TIMEOUT_SECS);
|
||||||
let result = tokio::time::timeout(
|
let result = tokio::time::timeout(
|
||||||
Duration::from_secs(DELEGATE_AGENTIC_TIMEOUT_SECS),
|
Duration::from_secs(agentic_timeout_secs),
|
||||||
run_tool_call_loop(
|
run_tool_call_loop(
|
||||||
provider,
|
provider,
|
||||||
&mut history,
|
&mut history,
|
||||||
@ -453,7 +457,7 @@ impl DelegateTool {
|
|||||||
success: false,
|
success: false,
|
||||||
output: String::new(),
|
output: String::new(),
|
||||||
error: Some(format!(
|
error: Some(format!(
|
||||||
"Agent '{agent_name}' timed out after {DELEGATE_AGENTIC_TIMEOUT_SECS}s"
|
"Agent '{agent_name}' timed out after {agentic_timeout_secs}s"
|
||||||
)),
|
)),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@ -530,6 +534,8 @@ mod tests {
|
|||||||
agentic: false,
|
agentic: false,
|
||||||
allowed_tools: Vec::new(),
|
allowed_tools: Vec::new(),
|
||||||
max_iterations: 10,
|
max_iterations: 10,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
agents.insert(
|
agents.insert(
|
||||||
@ -544,6 +550,8 @@ mod tests {
|
|||||||
agentic: false,
|
agentic: false,
|
||||||
allowed_tools: Vec::new(),
|
allowed_tools: Vec::new(),
|
||||||
max_iterations: 10,
|
max_iterations: 10,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
agents
|
agents
|
||||||
@ -697,6 +705,8 @@ mod tests {
|
|||||||
agentic: true,
|
agentic: true,
|
||||||
allowed_tools,
|
allowed_tools,
|
||||||
max_iterations,
|
max_iterations,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -805,6 +815,8 @@ mod tests {
|
|||||||
agentic: false,
|
agentic: false,
|
||||||
allowed_tools: Vec::new(),
|
allowed_tools: Vec::new(),
|
||||||
max_iterations: 10,
|
max_iterations: 10,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let tool = DelegateTool::new(agents, None, test_security());
|
let tool = DelegateTool::new(agents, None, test_security());
|
||||||
@ -911,6 +923,8 @@ mod tests {
|
|||||||
agentic: false,
|
agentic: false,
|
||||||
allowed_tools: Vec::new(),
|
allowed_tools: Vec::new(),
|
||||||
max_iterations: 10,
|
max_iterations: 10,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let tool = DelegateTool::new(agents, None, test_security());
|
let tool = DelegateTool::new(agents, None, test_security());
|
||||||
@ -946,6 +960,8 @@ mod tests {
|
|||||||
agentic: false,
|
agentic: false,
|
||||||
allowed_tools: Vec::new(),
|
allowed_tools: Vec::new(),
|
||||||
max_iterations: 10,
|
max_iterations: 10,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let tool = DelegateTool::new(agents, None, test_security());
|
let tool = DelegateTool::new(agents, None, test_security());
|
||||||
@ -1220,4 +1236,226 @@ mod tests {
|
|||||||
handle.write().push(Arc::new(FakeMcpTool));
|
handle.write().push(Arc::new(FakeMcpTool));
|
||||||
assert_eq!(handle.read().len(), 2);
|
assert_eq!(handle.read().len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Configurable timeout tests ──────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_timeout_values_used_when_config_unset() {
|
||||||
|
let config = DelegateAgentConfig {
|
||||||
|
provider: "ollama".to_string(),
|
||||||
|
model: "llama3".to_string(),
|
||||||
|
system_prompt: None,
|
||||||
|
api_key: None,
|
||||||
|
temperature: None,
|
||||||
|
max_depth: 3,
|
||||||
|
agentic: false,
|
||||||
|
allowed_tools: Vec::new(),
|
||||||
|
max_iterations: 10,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: None,
|
||||||
|
};
|
||||||
|
assert_eq!(config.timeout_secs.unwrap_or(DELEGATE_TIMEOUT_SECS), 120);
|
||||||
|
assert_eq!(
|
||||||
|
config
|
||||||
|
.agentic_timeout_secs
|
||||||
|
.unwrap_or(DELEGATE_AGENTIC_TIMEOUT_SECS),
|
||||||
|
300
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_timeout_values_are_respected() {
|
||||||
|
let config = DelegateAgentConfig {
|
||||||
|
provider: "ollama".to_string(),
|
||||||
|
model: "llama3".to_string(),
|
||||||
|
system_prompt: None,
|
||||||
|
api_key: None,
|
||||||
|
temperature: None,
|
||||||
|
max_depth: 3,
|
||||||
|
agentic: false,
|
||||||
|
allowed_tools: Vec::new(),
|
||||||
|
max_iterations: 10,
|
||||||
|
timeout_secs: Some(60),
|
||||||
|
agentic_timeout_secs: Some(600),
|
||||||
|
};
|
||||||
|
assert_eq!(config.timeout_secs.unwrap_or(DELEGATE_TIMEOUT_SECS), 60);
|
||||||
|
assert_eq!(
|
||||||
|
config
|
||||||
|
.agentic_timeout_secs
|
||||||
|
.unwrap_or(DELEGATE_AGENTIC_TIMEOUT_SECS),
|
||||||
|
600
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn timeout_deserialization_defaults_to_none() {
|
||||||
|
let toml_str = r#"
|
||||||
|
provider = "ollama"
|
||||||
|
model = "llama3"
|
||||||
|
"#;
|
||||||
|
let config: DelegateAgentConfig = toml::from_str(toml_str).unwrap();
|
||||||
|
assert!(config.timeout_secs.is_none());
|
||||||
|
assert!(config.agentic_timeout_secs.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn timeout_deserialization_with_custom_values() {
|
||||||
|
let toml_str = r#"
|
||||||
|
provider = "ollama"
|
||||||
|
model = "llama3"
|
||||||
|
timeout_secs = 45
|
||||||
|
agentic_timeout_secs = 900
|
||||||
|
"#;
|
||||||
|
let config: DelegateAgentConfig = toml::from_str(toml_str).unwrap();
|
||||||
|
assert_eq!(config.timeout_secs, Some(45));
|
||||||
|
assert_eq!(config.agentic_timeout_secs, Some(900));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_validation_rejects_zero_timeout() {
|
||||||
|
let mut config = crate::config::Config::default();
|
||||||
|
config.agents.insert(
|
||||||
|
"bad".into(),
|
||||||
|
DelegateAgentConfig {
|
||||||
|
provider: "ollama".into(),
|
||||||
|
model: "llama3".into(),
|
||||||
|
system_prompt: None,
|
||||||
|
api_key: None,
|
||||||
|
temperature: None,
|
||||||
|
max_depth: 3,
|
||||||
|
agentic: false,
|
||||||
|
allowed_tools: Vec::new(),
|
||||||
|
max_iterations: 10,
|
||||||
|
timeout_secs: Some(0),
|
||||||
|
agentic_timeout_secs: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let err = config.validate().unwrap_err();
|
||||||
|
assert!(
|
||||||
|
format!("{err}").contains("timeout_secs must be greater than 0"),
|
||||||
|
"unexpected error: {err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_validation_rejects_zero_agentic_timeout() {
|
||||||
|
let mut config = crate::config::Config::default();
|
||||||
|
config.agents.insert(
|
||||||
|
"bad".into(),
|
||||||
|
DelegateAgentConfig {
|
||||||
|
provider: "ollama".into(),
|
||||||
|
model: "llama3".into(),
|
||||||
|
system_prompt: None,
|
||||||
|
api_key: None,
|
||||||
|
temperature: None,
|
||||||
|
max_depth: 3,
|
||||||
|
agentic: false,
|
||||||
|
allowed_tools: Vec::new(),
|
||||||
|
max_iterations: 10,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: Some(0),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let err = config.validate().unwrap_err();
|
||||||
|
assert!(
|
||||||
|
format!("{err}").contains("agentic_timeout_secs must be greater than 0"),
|
||||||
|
"unexpected error: {err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_validation_rejects_excessive_timeout() {
|
||||||
|
let mut config = crate::config::Config::default();
|
||||||
|
config.agents.insert(
|
||||||
|
"bad".into(),
|
||||||
|
DelegateAgentConfig {
|
||||||
|
provider: "ollama".into(),
|
||||||
|
model: "llama3".into(),
|
||||||
|
system_prompt: None,
|
||||||
|
api_key: None,
|
||||||
|
temperature: None,
|
||||||
|
max_depth: 3,
|
||||||
|
agentic: false,
|
||||||
|
allowed_tools: Vec::new(),
|
||||||
|
max_iterations: 10,
|
||||||
|
timeout_secs: Some(7200),
|
||||||
|
agentic_timeout_secs: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let err = config.validate().unwrap_err();
|
||||||
|
assert!(
|
||||||
|
format!("{err}").contains("exceeds max 3600"),
|
||||||
|
"unexpected error: {err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_validation_rejects_excessive_agentic_timeout() {
|
||||||
|
let mut config = crate::config::Config::default();
|
||||||
|
config.agents.insert(
|
||||||
|
"bad".into(),
|
||||||
|
DelegateAgentConfig {
|
||||||
|
provider: "ollama".into(),
|
||||||
|
model: "llama3".into(),
|
||||||
|
system_prompt: None,
|
||||||
|
api_key: None,
|
||||||
|
temperature: None,
|
||||||
|
max_depth: 3,
|
||||||
|
agentic: false,
|
||||||
|
allowed_tools: Vec::new(),
|
||||||
|
max_iterations: 10,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: Some(5000),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let err = config.validate().unwrap_err();
|
||||||
|
assert!(
|
||||||
|
format!("{err}").contains("exceeds max 3600"),
|
||||||
|
"unexpected error: {err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_validation_accepts_max_boundary_timeout() {
|
||||||
|
let mut config = crate::config::Config::default();
|
||||||
|
config.agents.insert(
|
||||||
|
"ok".into(),
|
||||||
|
DelegateAgentConfig {
|
||||||
|
provider: "ollama".into(),
|
||||||
|
model: "llama3".into(),
|
||||||
|
system_prompt: None,
|
||||||
|
api_key: None,
|
||||||
|
temperature: None,
|
||||||
|
max_depth: 3,
|
||||||
|
agentic: false,
|
||||||
|
allowed_tools: Vec::new(),
|
||||||
|
max_iterations: 10,
|
||||||
|
timeout_secs: Some(3600),
|
||||||
|
agentic_timeout_secs: Some(3600),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert!(config.validate().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_validation_accepts_none_timeouts() {
|
||||||
|
let mut config = crate::config::Config::default();
|
||||||
|
config.agents.insert(
|
||||||
|
"ok".into(),
|
||||||
|
DelegateAgentConfig {
|
||||||
|
provider: "ollama".into(),
|
||||||
|
model: "llama3".into(),
|
||||||
|
system_prompt: None,
|
||||||
|
api_key: None,
|
||||||
|
temperature: None,
|
||||||
|
max_depth: 3,
|
||||||
|
agentic: false,
|
||||||
|
allowed_tools: Vec::new(),
|
||||||
|
max_iterations: 10,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert!(config.validate().is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -914,6 +914,8 @@ mod tests {
|
|||||||
agentic: false,
|
agentic: false,
|
||||||
allowed_tools: Vec::new(),
|
allowed_tools: Vec::new(),
|
||||||
max_iterations: 10,
|
max_iterations: 10,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -705,6 +705,8 @@ impl ModelRoutingConfigTool {
|
|||||||
agentic: false,
|
agentic: false,
|
||||||
allowed_tools: Vec::new(),
|
allowed_tools: Vec::new(),
|
||||||
max_iterations: DEFAULT_AGENT_MAX_ITERATIONS,
|
max_iterations: DEFAULT_AGENT_MAX_ITERATIONS,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
next_agent.provider = provider;
|
next_agent.provider = provider;
|
||||||
|
|||||||
@ -566,6 +566,8 @@ mod tests {
|
|||||||
agentic: false,
|
agentic: false,
|
||||||
allowed_tools: Vec::new(),
|
allowed_tools: Vec::new(),
|
||||||
max_iterations: 10,
|
max_iterations: 10,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
agents.insert(
|
agents.insert(
|
||||||
@ -580,6 +582,8 @@ mod tests {
|
|||||||
agentic: false,
|
agentic: false,
|
||||||
allowed_tools: Vec::new(),
|
allowed_tools: Vec::new(),
|
||||||
max_iterations: 10,
|
max_iterations: 10,
|
||||||
|
timeout_secs: None,
|
||||||
|
agentic_timeout_secs: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
agents
|
agents
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user