feat(graph): warn when a timeout is disabled
A timeout: 0 on settings.timeout or on an agent/script/llm/rag node is the author's explicit opt-out of the wall-clock bound. It stays legal; the validator now emits one warning per zero so the choice is visible in --validate output. Non-zero and unset values do not warn (unset already means no bound for settings.timeout and llm nodes, matching the explicit-zero-only convention of the orchestration-limit warnings). Design §3.13 / C21.
This commit is contained in:
1 file changed
+158
@@ -178,6 +178,7 @@ impl GraphValidator {
|
|||||||
self.validate_max_concurrency(graph, &mut result);
|
self.validate_max_concurrency(graph, &mut result);
|
||||||
self.validate_max_concurrency_template(graph, &mut result);
|
self.validate_max_concurrency_template(graph, &mut result);
|
||||||
self.validate_orchestration_limits(graph, &mut result);
|
self.validate_orchestration_limits(graph, &mut result);
|
||||||
|
self.validate_timeouts(graph, &mut result);
|
||||||
self.validate_map_subgraphs(graph, &mut result);
|
self.validate_map_subgraphs(graph, &mut result);
|
||||||
self.validate_parallel_user_interaction(graph, &mut result);
|
self.validate_parallel_user_interaction(graph, &mut result);
|
||||||
self.validate_parallel_writes(graph, &mut result);
|
self.validate_parallel_writes(graph, &mut result);
|
||||||
@@ -584,6 +585,34 @@ impl GraphValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A `timeout: 0` is the author's explicit opt-out of the wall-clock
|
||||||
|
/// bound. It is legal; the warning only makes the choice visible.
|
||||||
|
fn validate_timeouts(&self, graph: &Graph, result: &mut ValidationResult) {
|
||||||
|
if graph.settings.timeout == Some(0) {
|
||||||
|
result.warning(ValidationError::new(
|
||||||
|
"settings.timeout: 0 disables the graph wall-clock bound; the run \
|
||||||
|
ends only when the graph completes, is aborted, or hits \
|
||||||
|
max_loop_iterations",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for (node_id, node) in &graph.nodes {
|
||||||
|
let kind = match &node.node_type {
|
||||||
|
NodeType::Agent(a) if a.timeout == Some(0) => "agent",
|
||||||
|
NodeType::Script(s) if s.timeout == 0 => "script",
|
||||||
|
NodeType::Llm(l) if l.timeout == Some(0) => "llm",
|
||||||
|
NodeType::Rag(r) if r.timeout == Some(0) => "rag",
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
result.warning(ValidationError::with_node(
|
||||||
|
node_id,
|
||||||
|
format!(
|
||||||
|
"timeout: 0 disables the wall-clock bound for {kind} node \
|
||||||
|
'{node_id}'; it runs until it returns"
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Rules for the per-item subgraph rooted at each map's `branch`. Every
|
/// Rules for the per-item subgraph rooted at each map's `branch`. Every
|
||||||
/// error is attributed to the map node; the offending branch node is
|
/// error is attributed to the map node; the offending branch node is
|
||||||
/// named in the message.
|
/// named in the message.
|
||||||
@@ -3170,6 +3199,135 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn timeout_warnings(result: &ValidationResult) -> Vec<&ValidationError> {
|
||||||
|
result
|
||||||
|
.warnings
|
||||||
|
.iter()
|
||||||
|
.filter(|w| w.message.contains("disables the"))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settings_timeout_zero_warns() {
|
||||||
|
let mut graph = graph_with(vec![("e", end_node("e"))], "e");
|
||||||
|
graph.settings.timeout = Some(0);
|
||||||
|
|
||||||
|
let result = validator().validate(&graph);
|
||||||
|
|
||||||
|
assert!(result.is_valid());
|
||||||
|
let w = timeout_warnings(&result);
|
||||||
|
assert_eq!(w.len(), 1, "{:?}", result.warnings);
|
||||||
|
assert_eq!(w[0].node_id, None);
|
||||||
|
assert_eq!(
|
||||||
|
w[0].message,
|
||||||
|
"settings.timeout: 0 disables the graph wall-clock bound; the run ends only when \
|
||||||
|
the graph completes, is aborted, or hits max_loop_iterations"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn agent_timeout_zero_warns() {
|
||||||
|
let mut a = agent_node("a", "worker", Some("end"));
|
||||||
|
if let NodeType::Agent(ref mut an) = a.node_type {
|
||||||
|
an.timeout = Some(0);
|
||||||
|
}
|
||||||
|
let graph = graph_with(vec![("a", a), ("end", end_node("end"))], "a");
|
||||||
|
|
||||||
|
let result = validator().validate(&graph);
|
||||||
|
|
||||||
|
let w = timeout_warnings(&result);
|
||||||
|
assert_eq!(w.len(), 1, "{:?}", result.warnings);
|
||||||
|
assert_eq!(w[0].node_id.as_deref(), Some("a"));
|
||||||
|
assert_eq!(
|
||||||
|
w[0].message,
|
||||||
|
"timeout: 0 disables the wall-clock bound for agent node 'a'; it runs until it returns"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn script_timeout_zero_warns() {
|
||||||
|
let mut s = script_node("s", "does-not-exist.py", None);
|
||||||
|
if let NodeType::Script(ref mut sn) = s.node_type {
|
||||||
|
sn.timeout = 0;
|
||||||
|
}
|
||||||
|
s.next = Some("end".into());
|
||||||
|
let graph = graph_with(vec![("s", s), ("end", end_node("end"))], "s");
|
||||||
|
|
||||||
|
let result = validator().validate(&graph);
|
||||||
|
|
||||||
|
let w = timeout_warnings(&result);
|
||||||
|
assert_eq!(w.len(), 1, "{:?}", result.warnings);
|
||||||
|
assert_eq!(w[0].node_id.as_deref(), Some("s"));
|
||||||
|
assert_eq!(
|
||||||
|
w[0].message,
|
||||||
|
"timeout: 0 disables the wall-clock bound for script node 's'; it runs until it returns"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn llm_timeout_zero_warns() {
|
||||||
|
let mut l = llm_node("l", None, Some("end"));
|
||||||
|
if let NodeType::Llm(ref mut ln) = l.node_type {
|
||||||
|
ln.timeout = Some(0);
|
||||||
|
}
|
||||||
|
let graph = graph_with(vec![("l", l), ("end", end_node("end"))], "l");
|
||||||
|
|
||||||
|
let result = validator().validate(&graph);
|
||||||
|
|
||||||
|
assert!(result.is_valid(), "errors: {:?}", result.errors);
|
||||||
|
let w = timeout_warnings(&result);
|
||||||
|
assert_eq!(w.len(), 1, "{:?}", result.warnings);
|
||||||
|
assert_eq!(w[0].node_id.as_deref(), Some("l"));
|
||||||
|
assert_eq!(
|
||||||
|
w[0].message,
|
||||||
|
"timeout: 0 disables the wall-clock bound for llm node 'l'; it runs until it returns"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rag_timeout_zero_warns() {
|
||||||
|
let mut r = rag_node("r", &["docs/"], true);
|
||||||
|
if let NodeType::Rag(ref mut rn) = r.node_type {
|
||||||
|
rn.timeout = Some(0);
|
||||||
|
}
|
||||||
|
let graph = graph_with(vec![("r", r), ("end", end_node("end"))], "r");
|
||||||
|
|
||||||
|
let result = validator().validate(&graph);
|
||||||
|
|
||||||
|
assert!(result.is_valid(), "errors: {:?}", result.errors);
|
||||||
|
let w = timeout_warnings(&result);
|
||||||
|
assert_eq!(w.len(), 1, "{:?}", result.warnings);
|
||||||
|
assert_eq!(w[0].node_id.as_deref(), Some("r"));
|
||||||
|
assert_eq!(
|
||||||
|
w[0].message,
|
||||||
|
"timeout: 0 disables the wall-clock bound for rag node 'r'; it runs until it returns"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nonzero_and_unset_timeouts_do_not_warn() {
|
||||||
|
let mut a = agent_node("a", "worker", Some("s"));
|
||||||
|
if let NodeType::Agent(ref mut an) = a.node_type {
|
||||||
|
an.timeout = Some(600);
|
||||||
|
}
|
||||||
|
let mut s = script_node("s", "does-not-exist.py", None);
|
||||||
|
s.next = Some("l".into());
|
||||||
|
let l = llm_node("l", None, Some("end"));
|
||||||
|
let mut graph = graph_with(
|
||||||
|
vec![("a", a), ("s", s), ("l", l), ("end", end_node("end"))],
|
||||||
|
"a",
|
||||||
|
);
|
||||||
|
graph.settings.timeout = Some(30);
|
||||||
|
|
||||||
|
let result = validator().validate(&graph);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
timeout_warnings(&result).is_empty(),
|
||||||
|
"non-zero and unset timeouts should not warn: {:?}",
|
||||||
|
result.warnings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn map_max_concurrency_zero_errors() {
|
fn map_max_concurrency_zero_errors() {
|
||||||
let mut map = map_node_basic("m", "br", Some("end"));
|
let mut map = map_node_basic("m", "br", Some("end"));
|
||||||
|
|||||||
Reference in new issue
Block a user