Skip to content

fix(workflow): detect subflow reference cycles at publish and bound the run call chain (issue #273) - #360

Merged
yuluo-yx merged 2 commits into
agentic-ai-java:mainfrom
konlue:feat/issue273-subflow-recursion-boundary
Nov 8, 2026
Merged

yuluo-yx merged 2 commits into
agentic-ai-java:mainfrom
konlue:feat/issue273-subflow-recursion-boundary

Conversation

@konlue

@konlue konlue commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Describe what this PR does / why we need it

Issue #273 reported that subflow execution resolves the referenced definition and recurses into executeSteps with no cycle check and no depth bound — a self-referencing definition publishes fine and dies with a JVM StackOverflowError at 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.publish now resolves the definition's subflow reference graph against the already published definitions and rejects cycles before storing anything. The graph nodes are id@version pairs, 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 a WorkflowSubflowRecursionException carrying 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 WorkflowSubflowBoundaryTest with 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

…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.

@yuluo-yx yuluo-yx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@yuluo-yx
yuluo-yx merged commit 9e5c95c into agentic-ai-java:main Nov 8, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG][P2] Workflow 应检测不受控 Subflow 递归并提供深度边界

2 participants