Repository navigation
Conversation
69da2fa to
59554c1
Compare
59554c1 to
9a754fe
Compare
9a754fe to
10da7c3
Compare
10da7c3 to
3d0dd68
Compare
sangkyoonnam
left a comment
There was a problem hiding this comment.
Local tests on ee2aeb8 failed at both spots below (details inline). I can share the tests if that helps. On JDK 21, I ran mvn -o -B -pl runtime -am test -Dtest='MixedFutureGatherTest#submit*+control*+completionBetween*' -Dsurefire.failIfNoSpecifiedTests=false three times. Each run executed 6 tests and the same 3 failed. Can a subagent submit fail before its slot is reserved on the real ToolCallAction path, as the first test models?
| index = owner.getCurrentCallIndex(); | ||
| CallResult saved = owner.getCallResultAt(index); | ||
| if (saved != null && !saved.matches(id)) { | ||
| owner.clearCallResultsFromAndPersist(index); |
There was a problem hiding this comment.
On recovery, a finished tool's record can be dropped here when the id at this index differs from the first run.
I modeled ToolCallAction.executeParallel by hand in a local test (not part of this PR). On the first run the subagent submit fails before it reserves its slot, so the tool lands at index 0 as tool-call:call-2. On recovery the submit succeeds, index 0 is now session#call-1, saved.matches(id) is false, and clearCallResultsFromAndPersist(index) removes the tool's terminal record. The tool then runs again (toolCalls is 2, expected 1).
I ran submitFailingBeforeSlotReservationOnFirstRunKeepsCompletedToolOnRecovery and submitPersistenceFailureOnFirstRunKeepsCompletedToolOnRecovery. Both failed in 3 of 3 runs. The controls controlNoSubmitFailureKeepsCompletedToolOnRecovery and controlRecordedSubmitFailureKeepsCompletedToolOnRecovery passed in 3 of 3 runs.
The test doesn't go through the real ToolCallAction, so I haven't shown the first-run state is reachable there. Possible direction (inference): make a failed submit still consume its slot so the first run and recovery agree on indices, or look the record up by id instead of truncating the suffix.
There was a problem hiding this comment.
Thanks for catching this. I confirmed the path through the real ToolCallAction and operator recovery: a user-provided deferred prepare() or overridden async submitRequest() can throw before the durable call is created.
Fixed in 56a20dd using your first direction. The framework now reserves the call's durable slot before invoking preparation, records ordinary preparation failures in that same slot, and skips preparation when replaying a terminal outcome. The same handling covers awaitResult() preparation. Framework faults and cancellation still propagate without becoming terminal failure records.
SubagentPreparationRecoveryTest covers sequential and parallel tool dispatch, with a crash after the following ordinary tool's result is persisted. Recovery replays the preparation failure and reuses the tool result without executing it again. It also covers pending-state persistence failures. The focused suites pass on JDK 17 and 21.
| if (isDone()) { | ||
| return getCompletedOutcome(); | ||
| } | ||
| if (timeout != null) { |
There was a problem hiding this comment.
A call that completes after the sweep but before this finish(timeout) is recorded as a timeout failure.
isDone() only says whether the outcome is settled, not whether completion finished. So timeout != null builds Outcome.failure(timeout) and, for a started durable call, writes it with slot.finish(result). cancel() can't undo it, because completion.cancel(true) has no effect on a finished completion. The description says the scheduler "preserves available completions at timeout"; this window is after the sweep. The finish(timeout) branch is the same as before 0cecf5f, and the test still failed on ee2aeb8.
I injected that window in a local test (not part of this PR): a hook's preventStart() waits for the tool to finish, then sleeps 100 ms. completionBetweenSweepAndTimeoutFinishIsNotRecordedAsTimeout failed in 3 of 3 runs (isFailure() was true, expected false). The control controlCompletionBeforeDeadlineIsRecordedAsSuccess passed in 3 of 3 runs.
The window is narrow and I injected it by hand. Possible direction (inference): when timeout != null but completion already finished normally, take the finish(null) path, and keep canceled completions as timeouts.
There was a problem hiding this comment.
Thanks, confirmed and fixed in 8ebe3ad. After cancellation, the shared scheduler rechecks each completion signal. If it completed and was not cancelled, we use finish(null) to preserve the actual result or exception; cancelled or unfinished calls still take the timeout path.
Added deterministic race tests in MixedFutureGatherTest for successful completion, ordinary failure, fatal Error, and Chat/internal-subagent completion between the timeout sweep and cancellation. They pass on JDK 21, including the latest rerun.
Use a shared scheduling lifecycle for individual awaits and gather, with ordered durable reservations, recovery, cancellation and timeout handling. Submit ordinary calls to the managed async pool on JDK 11/17 as well as JDK 21, retaining inline execution for continuation-native callables. Keep Chat, subagent and tool entry points unchanged in this commit. Includes regression coverage for pending records, failure identity and parallel action-worker saturation. Part of apache#1338; Java only. Generated-by: Codex CLI 0.153.4 (GPT-6; exact model version unavailable) Co-authored-by: Codex <codex@openai.com>
Share SubgraphFuture between Chat and internal subagent calls. Delegate external subagent execution to managed futures while retaining subagent identity, cancellation and cleanup. Route subagent groups through gather. Add result mapping and mixed-call tests covering ordered durable slots, replay, partial recovery and cleanup. Part of apache#1338; Java only. Generated-by: Codex CLI 0.153.4 (GPT-6; exact model version unavailable) Co-authored-by: Codex <codex@openai.com>
Add the plan-layer AsyncTool contract and move SubagentTool into runtime. Resolve and bind subagent tools through ResourceContext. Let ToolCallAction gather ordinary durable tool calls and asynchronous subagent calls through the shared Future scheduler, preserving response IDs and reporting. Include resource-binding, tool execution and reporting regressions. Part of apache#1338. Python alignment follows after the Java changes merge. Generated-by: Codex CLI 0.153.4 (GPT-6; exact model version unavailable) Co-authored-by: Codex <codex@openai.com>
Remove executeAllAsync, the old batch driver and result wrapper, and the unused ContinuationNativeCallable marker. Keep managed worker submission and use the shared Future scheduler for batch execution. Migrate scheduling and lifecycle regressions to the remaining execution paths. Validated focused suites on JDK 17 (239 tests, 34 skipped) and JDK 21 (268 tests, 53 skipped), with no failures. Spotless checks pass. Generated-by: Codex CLI 0.153.4 (GPT-6; exact model version unavailable) Co-authored-by: Codex <codex@openai.com>
Retain cancelled graph invocations until record cleanup and discard their late events without scheduling successors. Allow already scheduled actions to retire normally while preserving errors for unknown calls and invalid events after normal completion. Add operator regressions for Chat and internal subagents, covering timeout, late output, registry cleanup and subsequent records. Focused suites pass on JDK 17 (117 tests) and JDK 21 (38 tests). Generated-by: Codex CLI 0.153.4 (GPT-6; exact model version unavailable) Co-authored-by: Codex <codex@openai.com>
Recheck completion signals after cancellation so completed calls retain their real outcomes. Cover durable success, business failure, fatal failure, Chat and internal subagent completions, including replay. Generated-by: Codex CLI 0.153.4 (GPT-6; exact model version unavailable) Co-authored-by: Codex <codex@openai.com>
Mark scheduler wait and internal-subagent guard failures as framework faults. Align subgraph fault assertions and operator recovery fixtures with pending slot reservation and executable subagent tools. Validation: focused API, plan and runtime suites pass on JDK 17 (257 passed, 38 skipped) and JDK 21 (265 passed, 39 skipped). Spotless and git diff --check pass. Generated-by: Codex CLI 0.153.4 (GPT-6; exact model version unavailable) Co-authored-by: Codex <codex@openai.com>
Reserve durable identities before invoking deferred preparation, async submission preparation, and result-wait preparation. Persist ordinary preparation failures in the same slot as execution and skip preparation when replaying terminal outcomes. Preserve mailbox execution, pending-call reconciliation, and fatal failure propagation. Add operator recovery coverage for sequential and parallel tool calls, plus preparation lifecycle tests. Document the reserved callable identity contract. Validation: JDK 17 focused tests: 172 passed, 15 skipped; JDK 21 focused tests: 241 passed, 3 skipped. Spotless and git diff --check pass. Generated-by: Codex CLI 0.153.4 (GPT-6; exact model version unavailable) Co-authored-by: Codex <codex@openai.com>
ee2aeb8 to
56a20dd
Compare
Expect the framework-fault wrapper for unsupported graph waits. Inject the routing recovery failure only after the selected durable slot is terminal, rather than when its pending record is first persisted. Declare the Python child tool schema and require an actual Python subgraph failure so tool resolution errors cannot satisfy the ordinary-failure assertion. Validation: focused Chat and routing recovery tests on JDK 11, 17 and 21; all 7 FrameworkFaultBridgeTest cases on JDK 21 with real Pemja and operators; Spotless and git diff --check. Generated-by: Codex CLI 0.153.4 (GPT-6; exact model version unavailable) Co-authored-by: Codex <codex@openai.com>
Linked issue: #1338 (Java implementation; Python follow-up after this PR merges).
Purpose of change
This PR changes Java implementation and tests only. Python will be aligned in a follow-up after the Java changes are merged. Unify individual await and mixed gather for ordinary, durable, Chat and subagent calls. ToolCallAction uses the same scheduler.
Runtime flow
Call execution
ToolCallAction: parallel path
Handle creation does not always start work: external async subagent submission happens before its result handle is returned. SubagentTool maps its handle to ToolResponse inside callAsync. Future inheritance, delegation and scheduler participation are detailed in the expandable section under Tests.
Key decisions
Behavioral Semantics
Interaction decisions
Behavioral contracts
Failure behavior
Ordinary callable exceptions become Outcome failures in gather and are rethrown by individual await. The scheduler propagates cancellation/interruption, UncapturableException and directly thrown Errors without recording terminal failures. An explicitly thrown CompletionException remains the original business exception, even when its cause is an Error. Timeout cancels unfinished work: started durable callables record timeout outcomes; unstarted callables and incomplete graphs retain pending records.
ToolCallAction converts non-cancellation Exceptions, including preparation/normalization failures, to tool responses. Invalid/cyclic subagent results become tool failures; cancellation and fatal Errors propagate. Exception classification in #1322 remains separate.
Tests
JDK 11: API/plan/runtime full suite: 2,994 tests, 76 skipped, zero failures/errors; Spotless passed. JDK 17: targeted API/plan/runtime regression: 488 tests, 46 skipped, zero failures/errors. JDK 21: clean targeted runtime and packaged integration run: 82 tests, one skipped, zero failures/errors.
Not verified: full Python/Java API parity, the complete repository suite, all live provider services, or cross-version savepoint upgrades. Python alignment is intentionally deferred.
Future relationships, implementation invariants and verification details
Future inheritance
Solid arrows mean
extends; dashed arrows meanimplements.SchedulableFutureis the runtime scheduling contract;SingleDurableFutureinherits its implementation fromSingleAsyncFuture.classDiagram direction TB class AsyncFuture { <<interface>> } class DurableFuture { <<interface>> } class SchedulableFuture { <<interface>> } class AsyncFutureImpl { <<abstract>> } class SubagentFuture { <<abstract>> } AsyncFuture <|-- DurableFuture AsyncFuture <|-- SchedulableFuture AsyncFuture <|.. AsyncFutureImpl AsyncFuture <|.. SubagentFuture AsyncFutureImpl <|-- SingleAsyncFuture SingleAsyncFuture <|-- SingleDurableFuture DurableFuture <|.. SingleDurableFuture AsyncFutureImpl <|-- SubgraphFuture DurableFuture <|.. SubgraphFuture AsyncFutureImpl <|-- MappedAsyncFuture AsyncFutureImpl <|-- GatherAsyncFuture SubagentFuture <|-- SubagentFutureImpl SubagentFuture <|-- CompletedSubagentFuture SchedulableFuture <|.. SingleAsyncFuture SchedulableFuture <|.. SubgraphFuture SchedulableFuture <|.. MappedAsyncFuture SchedulableFuture <|.. SubagentFutureImpl SchedulableFuture <|.. CompletedSubagentFutureAsyncFutureImplsupplies shared await/result caching.SubagentFutureImplretains the subagent API and delegates execution; it does not inherit fromAsyncFutureImpl.SubgraphFutureserves both Chat and internal subagents.Future delegation and scheduling
SchedulableFutureextendsAsyncFutureand defines the runtime lifecycle. Its implementations are SingleAsyncFuture (inherited by SingleDurableFuture), SubgraphFuture, MappedAsyncFuture, SubagentFutureImpl and CompletedSubagentFuture. CompletedSubagentFuture supplies an already-available result without starting work.GatherAsyncFuture does not implement SchedulableFuture. It represents the batch result; the scheduler operates on its participants. Nested gather handles are therefore rejected.
Review order and verification
Each commit includes its related tests. Split validation: first commit on JDK 11 (617 tests, 44 skipped); second commit on JDK 21 including clean-packaged integration tests (409 tests, 17 skipped); third commit on JDK 17 (655 tests, 57 skipped). All selected tests passed. Timeout-test synchronization was additionally verified on JDK 17: all 53 durable async context tests passed, followed by three successful repetitions of the three timeout cases.
DurableCallSlot reserves journal positions during prepare in input order; finish writes to the reserved index. Mapping shares underlying call identity and does not allocate another slot. A call-ID mismatch truncates the recorded suffix before reserving the new call. The scheduler fences queued work before cancellation, preserves available completions at timeout, and closes participants on every exit.
The JDK 11/17 saturation tests distinguish action workers waiting in await from callable bodies executing in the async pool. With two async workers, all action workers can wait while only two callables run. Existing checkpoint, drain, lifecycle and restore assertions remain in place. DecisionModel recovery tests capture pending state on cancellation and inject faults after terminal outcomes are persisted.
Clean reactor rebuilds remove stale Java 21 classes and previously shaded distribution contents. Tests use the Byte Buddy agent and repository Python environment for Java/Python bridge tests. JDK 21 skips Spotless; formatting is checked on JDK 11.
API
SubagentFuture implements AsyncFuture; DurableFuture inherits await. ResourceContext implementations must provide getSubagentTool. SubagentTool and its name prefix move out of API.
Tool.call and RunnerContext entry-point signatures stay unchanged. ctx.chat still returns ChatResult and throws for failed ChatResponseEvent.
Single async durable calls now reserve pending state before execution, including calls without a reconciler. On JDK 11/17, ordinary single async calls use the same worker pool as gathered calls. Python follows Java merge.
Documentation
doc-neededdoc-not-neededdoc-includedWas this patch authored or co-authored using generative AI tooling?
Generated-by: Codex CLI 0.153.4 (GPT-6; exact model version unavailable)