Skip to content

[api][plan][runtime][java][python] Support bounded internal sub-agent batches - #1199

Draft
dangzitou wants to merge 6 commits into
apache:mainfrom
dangzitou:feat/1140-batch-subagents
Draft

dangzitou wants to merge 6 commits into
apache:mainfrom
dangzitou:feat/1140-batch-subagents

Conversation

@dangzitou

@dangzitou dangzitou commented Oct 3, 2026 •

Copy link
Copy Markdown

Linked issue: #1140

This draft is stacked on #1138 and includes its internal sub-agent framework prerequisite. The batch-specific change is 9ad3a7b1.

Purpose of change

Parents can combine internal sub-agent handles into a durable batch, run a bounded fan-out across the same or different children, and receive ordered per-call results. This removes serial resolution and shared-worker starvation from the grouped invocation path.

Runtime flow

submit allocates deterministic identities without starting internal children. Group resolution validates admission, prepares deferred handles, and gathers their durable futures. The runtime reserves every outstanding slot, starts a bounded window on the mailbox, suspends the parent, persists each terminal result before admitting its replacement, and returns results in handle order. Recovery replays the parent first to reconstruct child scopes and reuses child action journals.

Key decisions

  • Reuse the existing group API and durable batch machinery; internal completion uses futures/coroutine polling instead of blocking async workers.
  • Bound active invocations per gathered batch, including different resource names. Nested batches have independent capacity so waiting ancestors cannot starve descendants.
  • Bound queued state by rejecting an oversized group before reservation; queued calls use existing PENDING slots rather than a second journal format.

Behavioral Semantics

Interaction decisions

Conditions Behavior
Internal or mixed internal/deferred group One sub-agent window; tool-call parallelism and timeout do not apply.
External deferred group only Existing tool-batch scheduling options apply.
Already submitted async handles Await existing runs; grouping cannot retroactively control their submission.
Completed and outstanding handles Reuse completed results; schedule outstanding deferred calls in their original order.
Nested internal group Its own window; the ancestor occupies only its parent's batch slot.
Child failure with unfinished actions/events Retain the slot until the child scope quiesces, then return its failed result.

Behavioral contracts

  1. A group returns one result per handle, in input order, after all child calls settle; successful siblings survive child failures.
  2. Internal groups admit at most subagent.parallelism invocations at once and work with one shared async worker, including nested calls.
  3. Oversized groups, duplicate handles, different owning contexts, and non-positive limits fail before internal preparation/reservation.
  4. Recovery preserves terminal results and replays running/queued calls under their original identities, including intermediate child events and failure summaries.
  5. Cancellation and infrastructure errors propagate from resolution; infrastructure errors leave unfinished durable slots pending.

Failure behavior

Admission errors raise. Java internal calls require JDK 21+ continuation support; Python callers must be async. Child action failures become SubagentResult.error after quiescence and their summaries survive replay. Bootstrap, polling, serialization, and journal errors propagate rather than becoming child results. Unfinished external effects may replay under the existing durability model, so callers still need idempotency or a reconciler.

Tests

Contract Tests
Ordered results and partial failures InternalSubagentBatchTest, Python test_internal_subagent_batch, Java/Python batch E2E
Bounded concurrency, one worker, nesting Java/Python batch E2E; existing nested-call tests with parallelism/worker count 1
Admission without side effects Invalid-group cases in Java/Python batch unit tests
Terminal/running/queued recovery InternalSubagentRecoveryTest, Java checkpoint/restart E2E, Python partial-result replay test
Exceptions versus durable child results Java bootstrap-failure/quiescence tests; Python bootstrap/polling-failure tests; existing cancellation tests

Coverage targets scheduling, worker starvation, durable slot indexing, checkpoint queue rotation, nested event replay, and the interval between child failure and parent result persistence.

Not verified: batch recovery against a production Kafka/Fluss journal, process/TaskManager loss, or rescaling across TaskManagers. The Python MiniCluster restart verification is local/manual and is not a committed CI regression.

Implementation invariants and verification evidence

All slots are reserved before bootstrap. Admission counts submitted slots until their completion callback persists them. Recovery drops checkpointed child tasks and regenerates their graph from the waiting parent, avoiding dispatch before transient scopes exist. Completed actions replay same-call forwarded events and outputs, but not nested bootstrap commands. Failed actions persist their failure summary and release discarded event counts. Nested AgentPlan JSON now closes its root object explicitly so resource-provider type markers remain in the correct object.

Core verification uses JDK 21, Python 3.12 and Flink 2.3. Distribution artifacts are rebuilt and copied to the Python library directories as in tools/build.sh.

  • mvn -B --no-transfer-progress -pl runtime -am -Dspotless.skip=true test: 2,190 tests, 39 skipped, no failures.
  • ./tools/ut.sh -p: 1,670 passed, 14 skipped; final core regression: 1,289 passed, 14 skipped.
  • Java InternalSubagentBatchE2ETest: both cases pass locally on Flink 1.20, 2.0 and 2.3 with JDK 21 and in CI on Flink 2.0/2.1/2.2; older Flink uses java.util/java.nio module opens. Fixtures use shared source/collector APIs. Java 17 + Flink 1.20 discovers both JRE-gated cases and passes 5 existing integration/serialization cases on three consecutive runs.
  • Python internal_subagent_test.py: single call, failure, nested call and bounded 12-call mixed-target batch.
  • Local Python-parent MiniCluster restart fixture on Flink 2.3.0, JDK 21.0.12.1 and Python 3.12.14: 6/6 runs (three successes and three cached child failures). Failure follows a completed checkpoint with one terminal, two running and one queued call. The agent operator restores, original call identities match, child invocation counts are [1,2,2,1], and ordered results/failures survive. The journal uses production ActionState serialization in the same JVM; a test-only close hook releases parked Python work during task shutdown. This fixture is uncommitted.
  • ./tools/lint.sh -c with JDK 17; ./tools/check-license.sh; tools/check-agents-md.py.

API

No invocation signatures change. Combining deferred handles now resolves one durable batch; sequential waits remain sequential. New Java/Python/YAML configuration keys are subagent.parallelism (16) and subagent.max-batch-size (1024), both positive. These bound call counts per group, not payload bytes or total job concurrency. Single internal waits use the same scheduler. External async submission remains unchanged.

Documentation

  • doc-needed
  • doc-not-needed
  • doc-included

Was this patch authored or co-authored using generative AI tooling?

  • Yes
  • No

Generated-by: Codex CLI 0.154.0 (GPT-6)

yunfengzhou-hub and others added 4 commits September 30, 2026 16:18
… internal sub-agent

Registering an Agent instance -- rather than a SubagentSetup -- as an AGENT
resource now compiles that child Agent into an in-process internal sub-agent.

AgentPlan detects `value instanceof Agent`, compiles the child plan once through
InternalSubagentCompilationHelper (child plans are reused across names and
cycles are rejected) and serves it via InternalSubagentProvider; the runtime
materializes it as InternalSubagentSetup. The caller drives the child directly
with submit(ctx, prompt).await(): an InternalSubagentCallEvent is sent, the
operator dispatches the child's actions in-process -- on a JDK 21 continuation
for the Java caller path and through the await-only resolve for the Python path
-- and the child's OutputEvents accumulate into the caller's Result. The child
context is scope-aware, so a nested child resolves against the child plan and
can itself register a grandchild. Calls are durable and failover-reproducible.

Generated-by: Qoder
Co-Authored-By: Qoder <noreply@qoder.com>
AI-Contributed/Feature: 1959/1959
AI-Contributed/UT: 788/788
…gents

Exercise the whole operator round-trip for a child Agent registered directly as
an AGENT resource: the Python-compiled child plan is materialized into a Java
InternalSubagentSetup, the call event is sent on the mailbox thread, the mailbox
is released while the caller awaits, and the child's OutputEvents accumulate
into the caller's Result. Covers the happy path, a failing child surfaced through
the Result, and a nested grandchild resolved against the child plan. submit is
called without identities, so the framework assigns the (session_id, call_id)
pair deterministically from the executing action task and replays it identically
after a failover. The Python caller path needs no JDK 21: a coroutine releases
the mailbox by yielding at await.

Generated-by: Qoder
Co-Authored-By: Qoder <noreply@qoder.com>
AI-Contributed/Feature: 0/0
AI-Contributed/UT: 183/183
…ask lifecycle

A child Agent registered as an AGENT resource compiles to a
PythonSerializableResourceProvider that the runtime treats as Java-owned, so
registerSubagentSetups left pythonSetupRegistered false and never added the
PythonTaskLifecycleListener. The caller-facing Python handle, built lazily during
the action body, then never observed onActionPrepared, and a no-id
submit(ctx, prompt) failed with "No prepared action task to assign sub-agent ids
from".

Register the bridge listener whenever the root plan declares a Python-compiled
sub-agent (hasPythonCompiledSubagent), so the lifecycle also reaches child-scope
actions. On the Python side, FlinkRunnerContext extracts the prepared namespace
eagerly in notify_action_prepared/transferred (plain values, no pemja reference
retained) and, in get_resource, wires a lazily materialized sub-agent handle as a
listener and replays that namespace via the new
BaseSubagentSetup.adopt_prepared_namespace. A no-id submit then mints
deterministic (session_id, call_id) identities, replayed identically after a
failover. Externally registered setups, already wired eagerly, are unaffected.

Generated-by: Qoder
Co-Authored-By: Qoder <noreply@qoder.com>
AI-Contributed/Feature: 87/87
AI-Contributed/UT: 0/0
@github-actions github-actions Bot added doc-included Your PR already contains the necessary documentation updates. fixVersion/0.4.0 priority/major Default priority of the PR or issue. and removed doc-included Your PR already contains the necessary documentation updates. labels Oct 3, 2026
dangzitou and others added 2 commits October 3, 2026 14:42
… batches

Schedule grouped internal calls on the mailbox after reserving durable slots. Persist each completed outcome before admitting queued work and reconstruct child scopes from the parent on recovery.

Generated-by: Codex CLI 0.154.0 (GPT-6)
Co-authored-by: Codex <noreply@openai.com>
Replace Flink 2.x legacy source and sink fixtures with the shared sequence source and async collector so Flink 1.20 can discover the JRE-gated tests. Keep the checkpoint-triggered restart and ordered batch assertions.

Generated-by: Codex CLI 0.154.0 (GPT-6)
@dangzitou
dangzitou force-pushed the feat/1140-batch-subagents branch from 4dee980 to 3f06f34 Compare October 3, 2026 06:43
@github-actions github-actions Bot added doc-included Your PR already contains the necessary documentation updates. and removed doc-included Your PR already contains the necessary documentation updates. labels Oct 3, 2026
@dangzitou
dangzitou marked this pull request as ready for review October 3, 2026 11:46
@yunfengzhou-hub

Copy link
Copy Markdown
Contributor

Hi @dangzitou , #1138 has been merged, and some prerequisites this PR bases on might have been changed. Could you please rebase your PR on the latest code of main branch and resolve the conflicts?

@dangzitou

Copy link
Copy Markdown
Author

Hi @yunfengzhou-hub, thanks for the heads-up.

After rebasing, I see that #1138 landed with a different internal-call model, and #1220 then moved internal sub-agent execution onto SubgraphInvocation (removing InternalSubagentCallStatus). So this PR can't be rebased mechanically: the admission and queueing logic needs to be rebuilt on the new invocation model.

Instead of one ~4.9k-line PR, I'd like to split #1140 into smaller PRs:

  1. Bounded admission for internal sub-agent invocations in the Java runtime: a concurrency option plus queueing/backpressure on top of SubgraphInvocation, with unit tests.
  2. Durable state for queued-but-not-started calls, and recovery of an in-flight batch.
  3. Python runtime parity.
  4. E2E tests and the Sub-Agents guide.

Two questions before I start:

I'll convert this PR to a draft and keep it for reference until the first split PR is up.

@dangzitou
dangzitou marked this pull request as draft October 20, 2026 18:25
@yunfengzhou-hub

Copy link
Copy Markdown
Contributor

Hi @yunfengzhou-hub, thanks for the heads-up.

After rebasing, I see that #1138 landed with a different internal-call model, and #1220 then moved internal sub-agent execution onto SubgraphInvocation (removing InternalSubagentCallStatus). So this PR can't be rebased mechanically: the admission and queueing logic needs to be rebuilt on the new invocation model.

Instead of one ~4.9k-line PR, I'd like to split #1140 into smaller PRs:

  1. Bounded admission for internal sub-agent invocations in the Java runtime: a concurrency option plus queueing/backpressure on top of SubgraphInvocation, with unit tests.
  2. Durable state for queued-but-not-started calls, and recovery of an in-flight batch.
  3. Python runtime parity.
  4. E2E tests and the Sub-Agents guide.

Two questions before I start:

I'll convert this PR to a draft and keep it for reference until the first split PR is up.

Hi @dangzitou, Flink Agents already has configurations that present an upper bound to the concurrency of sub-agent invocations, like num-async-threads or async.batch.parallelism. The "bounded parallelism" requirement of #1140 can be satisfied by these existing limits for now, without introducing a new upper bound — which also means neither of the two questions (scope and granularity) needs to be answered at this point.

What do you think? And if we could set aside the code related to this upper bound, will this PR still be so big that it needs to be divided into multiple ones?

@wenjin272

Copy link
Copy Markdown
Contributor

My original goal in [#1348](#1348) was to move SubagentTool out of the API and reduce the subagent-specific logic in ToolCallAction. This led me to unify the Future implementations and ctx.gather scheduling, which also enables batching Chat and subagent calls.

This overlaps with the Java batching work here. My preference is to handle execution differences within the Future implementations and share the batch scheduler and limits, without introducing subagent-specific branches in the scheduler.

#1348 is Java-only for now; Python alignment will follow after it merges. Your review would be very welcome, especially around subagent batching and recovery.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc-included Your PR already contains the necessary documentation updates. fixVersion/0.4.0 priority/major Default priority of the PR or issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants