From af4b65b295050bd838d08c9647fc8d49e9c8aebd Mon Sep 17 00:00:00 2001 From: zyssyz123 <916125788@qq.com> Date: Fri, 17 Jul 2026 16:42:58 +0800 Subject: [PATCH] fix(agent): allow dangling refs in workflow drafts (#39192) --- .../workflow/nodes/agent_v2/validators.py | 29 +++++++++--- .../nodes/agent_v2/test_validators.py | 45 +++++++++++++++++++ 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/api/core/workflow/nodes/agent_v2/validators.py b/api/core/workflow/nodes/agent_v2/validators.py index 83dce82a58d..7e3969d8a16 100644 --- a/api/core/workflow/nodes/agent_v2/validators.py +++ b/api/core/workflow/nodes/agent_v2/validators.py @@ -61,16 +61,33 @@ class WorkflowAgentNodeValidator: @classmethod def validate_draft_workflow(cls, *, session: Session, workflow: Workflow) -> None: - cls._validate_workflow(session=session, workflow=workflow, require_binding=False) + cls._validate_workflow( + session=session, + workflow=workflow, + require_binding=False, + validate_previous_node_topology=False, + ) @classmethod def validate_published_workflow(cls, *, session: Session, workflow: Workflow) -> None: - cls._validate_workflow(session=session, workflow=workflow, require_binding=True) + cls._validate_workflow( + session=session, + workflow=workflow, + require_binding=True, + validate_previous_node_topology=True, + ) @classmethod - def _validate_workflow(cls, *, session: Session, workflow: Workflow, require_binding: bool) -> None: + def _validate_workflow( + cls, + *, + session: Session, + workflow: Workflow, + require_binding: bool, + validate_previous_node_topology: bool, + ) -> None: graph = workflow.graph_dict - topology = _WorkflowGraphTopology.from_graph(graph) + topology = _WorkflowGraphTopology.from_graph(graph) if validate_previous_node_topology else None for node_id, node_data in cls.iter_agent_v2_nodes(graph): cls._validate_node_schema(node_id=node_id, node_data=node_data) binding = cls._find_binding( @@ -185,12 +202,12 @@ class WorkflowAgentNodeValidator: raise WorkflowAgentNodeValidationError( f"Workflow Agent node {binding.node_id} has invalid previous node output ref." ) - if topology is None: - continue if len(selector) < 2: raise WorkflowAgentNodeValidationError( f"Workflow Agent node {binding.node_id} has incomplete previous node output ref." ) + if topology is None: + continue source_node_id = selector[0] if not topology.has_node(source_node_id): raise WorkflowAgentNodeValidationError( diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py index 0c8169943f9..237ab8ebe3b 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py @@ -187,6 +187,51 @@ def test_draft_validation_allows_unbound_agent_node(): ) +def test_draft_validation_allows_missing_previous_node(): + node_job = WorkflowNodeJobConfig.model_validate( + {"previous_node_output_refs": [{"node_id": "missing-node", "output": "text"}]} + ) + session = Mock() + session.scalar.side_effect = [_binding(node_job), _agent(), _snapshot()] + + WorkflowAgentNodeValidator.validate_draft_workflow( + session=session, + workflow=_workflow(_graph([{"source": "start", "target": "agent-node"}])), + ) + + +def test_draft_validation_allows_non_upstream_previous_output_ref(): + node_job = WorkflowNodeJobConfig.model_validate( + {"previous_node_output_refs": [{"node_id": "later-node", "output": "text"}]} + ) + session = Mock() + session.scalar.side_effect = [_binding(node_job), _agent(), _snapshot()] + + WorkflowAgentNodeValidator.validate_draft_workflow( + session=session, + workflow=_workflow( + _graph( + [ + {"source": "start", "target": "agent-node"}, + {"source": "agent-node", "target": "later-node"}, + ] + ) + ), + ) + + +def test_draft_validation_rejects_incomplete_previous_output_ref(): + node_job = WorkflowNodeJobConfig.model_validate({"previous_node_output_refs": [{"selector": ["previous-node"]}]}) + session = Mock() + session.scalar.side_effect = [_binding(node_job), _agent(), _snapshot()] + + with pytest.raises(WorkflowAgentNodeValidationError, match="incomplete previous node output ref"): + WorkflowAgentNodeValidator.validate_draft_workflow( + session=session, + workflow=_workflow(_graph([{"source": "start", "target": "agent-node"}])), + ) + + def test_publish_validation_requires_binding(): session = Mock() session.scalar.return_value = None