feat: auto continuation is unavailable in llm nodes; continuation driven loops should be in agent nodes only
CI / All (ubuntu-latest) (push) Failing after 28s
CI / All (macos-latest) (push) Canceled after 0s
CI / All (windows-latest) (push) Canceled after 0s

This commit is contained in:
Dark-Alex-17 committed 2026-09-15 11:42:39 -06:00
1 parent 8746e3e29a
commit 9e2f3c0f12
4 files changed
+167 -20

No files matched your search

+27
View File
@@ -12,6 +12,7 @@ use crate::function::ToolResult;
use crate::function::agents::{GuardrailAction, check_pending_tasks_guardrail};
use crate::function::jobs::reap_jobs;
use crate::function::skill::skill_function_declarations;
use crate::function::todo::TODO_FUNCTION_PREFIX;
use crate::utils::AbortSignal;
use anyhow::{Context, Error, Result, anyhow, bail};
use log::warn;
@@ -485,6 +486,14 @@ fn validate_tools_subset(
.map(|d| d.name.as_str())
.collect();
for name in regular {
if name.starts_with(TODO_FUNCTION_PREFIX) {
bail!(
"llm node cannot enable '{name}': todo tools drive chat turn loops \
(auto_continue), which llm nodes don't have. Node continuation is \
owned by the node loop, max_iterations, and graph edges. For driven \
multi-step work, use an agent node whose agent sets auto_continue: true."
);
}
if !known.contains(name.as_str()) {
let mut avail: Vec<&str> = known.iter().copied().collect();
avail.sort();
@@ -794,6 +803,24 @@ mod tests {
assert!(mcp.is_empty());
}
#[test]
fn validate_tools_subset_rejects_todo_tools() {
let mut ctx = plain_ctx();
let mut agent = Agent::test_new(AgentConfig {
auto_continue: true,
..AgentConfig::default()
});
agent.functions_mut().append_todo_functions();
ctx.agent = Some(agent);
let err = validate_tools_subset(&["todo__init".to_string()], &[], &ctx).unwrap_err();
assert!(
err.to_string().contains("todo tools drive chat turn loops"),
"expected the todo teaching error, got: {err}"
);
}
#[test]
fn zero_timeout_resolves_to_no_bound() {
assert!(Some(0u64).and_then(wall_clock).is_none());
+86
View File
@@ -3,6 +3,7 @@ use super::types::{ConcurrencyCap, Graph, NextTargets, Node, NodeType};
use crate::client::{Model, ModelType};
use crate::config;
use crate::config::{Agent, AppConfig, paths};
use crate::function::todo::TODO_FUNCTION_PREFIX;
use crate::function::user_interaction::USER_FUNCTION_PREFIX;
use crate::rag::{GraphRagConfig, RagData};
use anyhow::{Result, bail};
@@ -181,6 +182,7 @@ impl GraphValidator {
self.validate_fallback_capture(graph, &mut result);
self.validate_output_schema_properties(graph, &mut result);
self.validate_structured_output_user_tools(graph, &mut result);
self.validate_llm_node_todo_tools(graph, &mut result);
self.validate_max_concurrency(graph, &mut result);
self.validate_max_concurrency_template(graph, &mut result);
self.validate_orchestration_limits(graph, &mut result);
@@ -483,6 +485,28 @@ impl GraphValidator {
}
}
fn validate_llm_node_todo_tools(&self, graph: &Graph, result: &mut ValidationResult) {
for (node_id, node) in &graph.nodes {
let NodeType::Llm(llm) = &node.node_type else {
continue;
};
let exposes_todo_tools = llm.tools.as_ref().is_some_and(|tools| {
tools
.iter()
.any(|t| t.trim().starts_with(TODO_FUNCTION_PREFIX))
});
if exposes_todo_tools {
result.error(ValidationError::with_node(
node_id,
"llm node exposes todo tools (`todo__*`): todo tools drive chat turn \
loops (auto_continue), which llm nodes don't have. Node continuation \
is owned by the node loop, max_iterations, and graph edges. For driven \
multi-step work, use an agent node whose agent sets `auto_continue: true`",
));
}
}
}
fn validate_node_references(&self, graph: &Graph, result: &mut ValidationResult) {
for (node_id, node) in &graph.nodes {
for (target, label) in declared_targets(node) {
@@ -4793,6 +4817,68 @@ mod tests {
);
}
#[test]
fn structured_output_node_with_todo_tools_errors() {
let mut node = llm_with_output_schema("l", &["verdict"], Some("end"));
if let NodeType::Llm(ref mut n) = node.node_type {
n.tools = Some(vec!["todo__init".into()]);
}
let graph = graph_with(vec![("l", node), ("end", end_node("end"))], "l");
let result = validator().validate(&graph);
assert!(
result
.errors
.iter()
.any(|e| e.node_id.as_deref() == Some("l")
&& e.message.contains("todo tools drive chat turn loops")),
"expected todo-tool error: {:?}",
result.errors
);
}
#[test]
fn non_structured_node_with_todo_tools_errors() {
let mut node = llm_node("l", None, Some("end"));
if let NodeType::Llm(ref mut n) = node.node_type {
n.tools = Some(vec!["todo__pause".into()]);
}
let graph = graph_with(vec![("l", node), ("end", end_node("end"))], "l");
let result = validator().validate(&graph);
assert!(
result
.errors
.iter()
.any(|e| e.node_id.as_deref() == Some("l")
&& e.message.contains("todo tools drive chat turn loops")),
"expected todo-tool error: {:?}",
result.errors
);
}
#[test]
fn llm_node_without_todo_tools_does_not_error() {
let mut node = llm_node("l", None, Some("end"));
if let NodeType::Llm(ref mut n) = node.node_type {
n.tools = Some(vec!["user__select".into()]);
}
let graph = graph_with(vec![("l", node), ("end", end_node("end"))], "l");
let result = validator().validate(&graph);
assert!(
!result
.errors
.iter()
.any(|e| e.message.contains("todo tools drive chat turn loops")),
"unexpected todo-tool error: {:?}",
result.errors
);
}
#[test]
fn fallback_with_output_schema_and_empty_state_updates_warns() {
let mut node = llm_with_output_schema("l", &["summary"], Some("end"));