Repository navigation
fix(workflow): detect subflow reference cycles at publish and bound the run call chain (issue #273) - #360
Merged
yuluo-yx merged 2 commits intoNov 8, 2026
Conversation
…he run call chain (issue agentic-ai-java#273) - WorkflowRegistry.publish: resolve the definition's subflow reference graph against the published definitions and reject cycles with the reference chain (a@1 -> b@2 -> a@1); a reference to an unpublished definition stays a legal forward reference and surfaces at run time as usual - WorkflowAgent: the run carries a subflow call chain (id@version, survives suspensions, copied per parallel branch); entering a subflow already on the chain or past the configured depth limit fails with a WorkflowSubflowRecursionException naming the chain, instead of exhausting the JVM stack - Builder.subflowMaxDepth(int): the explicit bound for the chain, default 16 - legitimate reuse stays legal: sequential and parallel invocations of the same subflow are not on one call path - tests: WorkflowSubflowBoundaryTest, 8 cases (red 4F before, green after)
…nsion chain semantics Self-review: the cancellation test cancelled before entering the subflow, which only exercised the entry guard — the subflow now contains an agent step whose run cancels itself mid-execution. The executeSubflow javadoc documents that a suspension removes the entry from the chain on the way out: the resumed segment re-entering the same subflow is sequential reuse, still bounded by the chain and depth checks.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Describe what this PR does / why we need it
Issue #273 reported that subflow execution resolves the referenced definition and recurses into
executeStepswith no cycle check and no depth bound — a self-referencing definition publishes fine and dies with a JVMStackOverflowErrorat run time. This PR adds a two-line defense at publish time and at run time, entirely inside the workflow module (no Graph/Loop, no new dependencies).Publish-time cycle detection
WorkflowRegistry.publishnow resolves the definition's subflow reference graph against the already published definitions and rejects cycles before storing anything. The graph nodes areid@versionpairs, so referencing a different version of the same workflow is not a cycle. The rejection message carries the closed chain (a@1 -> b@2 -> a@1). A reference to a definition that is not published yet stays a legal forward reference — it does not block the publish, and an unresolved reference surfaces at run time as the usual "workflow version is not published" failure. This policy is documented in the registry javadoc.Run-time call chain and depth bound
The run now carries a subflow call chain (
id@version, caller first) on its run data: it survives suspensions through the continuation closures and is copied per parallel branch. Entering a subflow whose node is already on the chain closes a cycle and fails with aWorkflowSubflowRecursionExceptioncarrying the closed chain; a chain that has reached the configured depth limit fails with the same exception and a diagnosis that names the root definition and every level. Nothing exhausts the JVM stack — an overly deep but acyclic chain ends with a domain error.The limit defaults to 16 and is configurable through
Builder.subflowMaxDepth(int)— the explicit bound the issue asks for in place of an unconditional recursion ban.Legitimate reuse stays legal
The chain is the current call path, not a set of already-used subflows: invoking the same subflow twice sequentially, or from two parallel branches, is not a cycle and keeps working. Deliberate recursion through the DSL is not supported in this PR — a cycle on the call chain is always refused — because the issue's requirement is a bounded policy, and the depth limit plus publish-time rejection covers the failure modes without extending the definition format.
Testing
New
WorkflowSubflowBoundaryTestwith 8 cases: self-reference rejected at publish with the closed chain in the message, an indirect cycle across definitions rejected when the last definition closes it (the earlier forward reference publishes), a forward reference publishing successfully and failing at run with the usual unresolved error, a 20-level acyclic chain ending with the domain error instead of a stack overflow, the configured depth limit taking effect, sequential and parallel reuse of the same subflow completing, and cancellation inside a subflow terminating normally. Run against the unmodified code, 4 cases fail (both publish rejections and both depth cases); with the fix the whole argi-workflow-core module is 46/46 green, checkstyle clean.Fixes #273