This is an automated email from the ASF dual-hosted git repository.

wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git


The following commit(s) were added to refs/heads/main by this push:
     new eca1969fd [runtime][java][python] Release subagent handles after await 
failures (#1320)
eca1969fd is described below

commit eca1969fde826bfbfc41f146e51b5b1b7786217b
Author: Wenjin Xie <[email protected]>
AuthorDate: Fri Oct 23 16:04:50 2026 +0800

    [runtime][java][python] Release subagent handles after await failures 
(#1320)
    
    Untrack deferred and async subagent handles in finally blocks so handled
    model-driven await failures can reach action finalization without leaving
    pending calls. Preserve caller-driven exceptions, cancellation and 
interruption.
    
    Document the await failure contract and cover model dispatch, lifecycle
    finalization and direct exception propagation in both languages.
    
    Fixes #1279
    
    Generated-by: Codex 0.153.4 (GPT-6)
    
    Co-authored-by: Codex <[email protected]>
---
 .../flink/agents/api/subagent/SubagentFuture.java  |  11 +-
 python/flink_agents/api/subagent.py                |  10 +-
 python/flink_agents/runtime/async_subagent.py      |  28 +--
 python/flink_agents/runtime/deferred_subagent.py   |  21 +-
 .../runtime/tests/test_subagent_await_cleanup.py   | 163 ++++++++++++++
 .../runtime/subagent/AsyncSubagentFuture.java      |  16 +-
 .../runtime/subagent/DeferredSubagentFuture.java   |  14 +-
 .../runtime/subagent/SubagentAwaitCleanupTest.java | 247 +++++++++++++++++++++
 8 files changed, 473 insertions(+), 37 deletions(-)

diff --git 
a/api/src/main/java/org/apache/flink/agents/api/subagent/SubagentFuture.java 
b/api/src/main/java/org/apache/flink/agents/api/subagent/SubagentFuture.java
index 38e2eb8cd..005c2c2be 100644
--- a/api/src/main/java/org/apache/flink/agents/api/subagent/SubagentFuture.java
+++ b/api/src/main/java/org/apache/flink/agents/api/subagent/SubagentFuture.java
@@ -44,8 +44,15 @@ public abstract class SubagentFuture {
     public abstract boolean isDone();
 
     /**
-     * Resolves the invocation, waiting until it reaches a terminal state. 
Failures converge into a
-     * failed {@link SubagentResult} rather than a separately reported 
exceptional completion.
+     * Resolves the invocation, waiting until it reaches a terminal state. A 
comprehensible
+     * sub-agent failure is returned as a failed {@link SubagentResult}; 
system-level exceptions,
+     * cancellation and interruption propagate to the caller.
+     *
+     * <p>Do not retry an await that throws. Any retry belongs inside its 
await implementation.
+     * Successful repeated awaits may return the cached result. The built-in 
deferred and async
+     * handles release their pending-call registration when the await exits, 
including
+     * exceptionally; callers need no additional cleanup to finish an action 
after handling an await
+     * failure.
      */
     public abstract SubagentResult await() throws Exception;
 
diff --git a/python/flink_agents/api/subagent.py 
b/python/flink_agents/api/subagent.py
index 10b62f9e9..d08d56179 100644
--- a/python/flink_agents/api/subagent.py
+++ b/python/flink_agents/api/subagent.py
@@ -176,8 +176,14 @@ class SubagentFuture(ABC):
     def __await__(self) -> Any:
         """Resolve the invocation, waiting until it reaches a terminal state.
 
-        Failures converge into a failed :class:`SubagentResult` rather than a
-        separately raised exception.
+        Comprehensible sub-agent failures return a failed 
:class:`SubagentResult`;
+        system-level exceptions, cancellation and interruption propagate.
+
+        Do not retry an await that raises. Any retry belongs inside its 
implementation.
+        Successful repeated awaits may return the cached result. The built-in 
deferred
+        and async handles release their pending-call registration when the 
await
+        exits, including exceptionally, so
+        callers need no extra cleanup after handling an await failure.
         """
 
 
diff --git a/python/flink_agents/runtime/async_subagent.py 
b/python/flink_agents/runtime/async_subagent.py
index 585de4919..3a88d4c0c 100644
--- a/python/flink_agents/runtime/async_subagent.py
+++ b/python/flink_agents/runtime/async_subagent.py
@@ -143,19 +143,21 @@ class AsyncSubagentFuture(SubagentFuture):
             msg = f"Sub-agent call cancelled: {self.identity}"
             raise CancelledError(msg)
         if not self._consumed:
-            # Build the durable awaitable first, then yield from it, so the
-            # await composition and its durable execution cannot be misread
-            # as one serial call.
-            awaitable = self._ctx.durable_execute_async(
-                self._setup._await_until_terminal,
-                self.session_id,
-                self.call_id,
-                durable_id=f"{self.identity}#await",
-            )
-            self._value = yield from awaitable.__await__()
-            self._consumed = True
-            if self._registry is not None:
-                self._registry.untrack_pending_subagent_call(self.identity)
+            try:
+                # Build the durable awaitable first, then yield from it, so the
+                # await composition and its durable execution cannot be misread
+                # as one serial call.
+                awaitable = self._ctx.durable_execute_async(
+                    self._setup._await_until_terminal,
+                    self.session_id,
+                    self.call_id,
+                    durable_id=f"{self.identity}#await",
+                )
+                self._value = yield from awaitable.__await__()
+                self._consumed = True
+            finally:
+                if self._registry is not None:
+                    self._registry.untrack_pending_subagent_call(self.identity)
         return self._value
 
     def cancel(self) -> None:
diff --git a/python/flink_agents/runtime/deferred_subagent.py 
b/python/flink_agents/runtime/deferred_subagent.py
index 68024b604..43432cd28 100644
--- a/python/flink_agents/runtime/deferred_subagent.py
+++ b/python/flink_agents/runtime/deferred_subagent.py
@@ -96,13 +96,18 @@ class DeferredSubagentFuture(SubagentFuture):
         A system-level failure escaping durable execution propagates and fails
         the action instead of being folded into an error result.
         """
-        durable_id, call, reconcile = self.prepare()
-        value = yield from self._ctx.durable_execute_async(
-            call,
-            reconciler=reconcile,
-            durable_id=durable_id,
-        ).__await__()
-        self._resolve(value)
+        try:
+            durable_id, call, reconcile = self.prepare()
+            value = yield from self._ctx.durable_execute_async(
+                call,
+                reconciler=reconcile,
+                durable_id=durable_id,
+            ).__await__()
+            self._resolve(value)
+        finally:
+            # The dispatcher may handle the exception and finish the action.
+            if self._registry is not None:
+                self._registry.untrack_pending_subagent_call(self.identity)
 
     def combine(self, *others: SubagentFuture) -> SubagentFutures:
         """Group this handle with others for a batched resolve."""
@@ -120,8 +125,6 @@ class DeferredSubagentFuture(SubagentFuture):
     def _resolve(self, value: SubagentResult) -> None:
         self._value = value
         self._done = True
-        if self._registry is not None:
-            self._registry.untrack_pending_subagent_call(self.identity)
 
 
 class DeferredSubagentSetup(BaseSubagentSetup, ABC):
diff --git a/python/flink_agents/runtime/tests/test_subagent_await_cleanup.py 
b/python/flink_agents/runtime/tests/test_subagent_await_cleanup.py
new file mode 100644
index 000000000..ff2c92bc2
--- /dev/null
+++ b/python/flink_agents/runtime/tests/test_subagent_await_cleanup.py
@@ -0,0 +1,163 @@
+################################################################################
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
+"""Await failures must not leave handles pending at action finalization."""
+
+import asyncio
+from concurrent.futures import CancelledError
+from typing import Any
+
+import pytest
+from pydantic import PrivateAttr
+
+from flink_agents.api.subagent import CALLABLE_NAME_PREFIX, SubagentResult
+from flink_agents.plan.actions.tool_call_action import process_tool_request
+from flink_agents.plan.tests.actions.test_tool_call_action_subagent import (
+    _Context,
+    _request,
+)
+from flink_agents.runtime.async_subagent import BaseAsyncSubagentSetup, 
RunStatus
+from flink_agents.runtime.deferred_subagent import DeferredSubagentSetup
+from flink_agents.runtime.tests.test_base_subagent import _FakeTask, _run
+
+
+class _AwaitContext(_Context):
+    def durable_execute_async(self, func, *args: Any, **kwargs: Any) -> Any:
+        kwargs.pop("durable_id", None)
+        kwargs.pop("reconciler", None)
+
+        async def execute() -> Any:
+            return func(*args, **kwargs)
+
+        return execute()
+
+
+class _DeferredSetup(DeferredSubagentSetup):
+    _failure: BaseException | None = PrivateAttr(default=None)
+    _stage: str = PrivateAttr(default="call")
+
+    def prepare(self, ctx, prompt, session_id, call_id) -> tuple:
+        if self._stage == "prepare":
+            raise self._failure
+
+        def call() -> SubagentResult:
+            if self._failure is not None:
+                raise self._failure
+            return SubagentResult.error("remote declined")
+
+        return f"{session_id}#{call_id}", call, None
+
+
+class _AsyncSetup(BaseAsyncSubagentSetup):
+    _failure: BaseException | None = PrivateAttr(default=None)
+    _stage: str = PrivateAttr(default="status")
+
+    def call_submit_request(self, session_id, call_id, prompt) -> None:
+        pass
+
+    def call_query_status(self, session_id, call_id) -> RunStatus:
+        if self._failure is not None and self._stage == "status":
+            raise self._failure
+        return RunStatus.completed()
+
+    def call_fetch_result(self, session_id, call_id) -> SubagentResult:
+        if self._failure is not None:
+            raise self._failure
+        return SubagentResult.error("remote declined")
+
+
+def _setup(mode, stage, failure) -> _DeferredSetup | _AsyncSetup:
+    setup = _DeferredSetup() if mode == "deferred" else _AsyncSetup()
+    setup._stage = stage
+    setup._failure = failure
+    setup.set_subagent_name("reviewer")
+    return setup
+
+
[email protected]("parallel", [False, True])
[email protected](
+    ("mode", "stage"),
+    [
+        ("deferred", "prepare"),
+        ("deferred", "call"),
+        ("async", "status"),
+        ("async", "fetch"),
+    ],
+)
[email protected]("raises", [False, True])
+def test_model_handled_failure_allows_action_finalization(
+    mode, stage, parallel, raises
+):
+    failure = RuntimeError("remote unavailable") if raises else None
+    # A prepare failure has no result-returning variant.
+    setup = _setup(mode, stage if raises else "call", failure)
+    task = _FakeTask(action_name="tool_call_action")
+    setup.on_action_prepared(task)
+    ctx = _AwaitContext().with_agent("reviewer", setup)
+    if parallel:
+        ctx.with_parallel_tool_calls()
+    request = _request(
+        "model",
+        [
+            {
+                "id": f"call-{i}",
+                "function": {
+                    "name": CALLABLE_NAME_PREFIX + "reviewer",
+                    "arguments": {"code": "hello"},
+                },
+            }
+            for i in range(2)
+        ],
+    )
+
+    asyncio.run(process_tool_request(request, ctx))
+
+    response = ctx.sent_events[0]
+    for i in range(2):
+        assert response.success[f"call-{i}"] is False
+        assert (
+            "remote unavailable" if raises else "remote declined"
+        ) in response.error[f"call-{i}"]
+    # This is the lifecycle check that used to fail after dispatch returned.
+    setup.on_action_finishing(task)
+
+
[email protected](
+    ("mode", "stage"),
+    [
+        ("deferred", "prepare"),
+        ("deferred", "call"),
+        ("async", "status"),
+        ("async", "fetch"),
+    ],
+)
[email protected](
+    "failure_type",
+    [RuntimeError, CancelledError, asyncio.CancelledError, InterruptedError],
+)
+def test_direct_await_preserves_exception_and_releases_handle(
+    mode, stage, failure_type
+):
+    failure = failure_type("await failed")
+    setup = _setup(mode, stage, failure)
+    task = _FakeTask()
+    setup.on_action_prepared(task)
+    future = _run(setup.submit(_AwaitContext(), "hello"))
+    with pytest.raises(failure_type) as caught:
+        _run(future)
+    assert caught.value is failure
+    setup.on_action_finishing(task)
diff --git 
a/runtime/src/main/java/org/apache/flink/agents/runtime/subagent/AsyncSubagentFuture.java
 
b/runtime/src/main/java/org/apache/flink/agents/runtime/subagent/AsyncSubagentFuture.java
index 254e11896..707a612a5 100644
--- 
a/runtime/src/main/java/org/apache/flink/agents/runtime/subagent/AsyncSubagentFuture.java
+++ 
b/runtime/src/main/java/org/apache/flink/agents/runtime/subagent/AsyncSubagentFuture.java
@@ -84,12 +84,16 @@ final class AsyncSubagentFuture extends SubagentFuture {
                     "Sub-agent call cancelled: " + getSessionId() + "#" + 
getCallId());
         }
         if (!consumed) {
-            DurableCallable<SubagentResult> awaitCall =
-                    setup.awaitResult(ctx, getSessionId(), getCallId());
-            value = ctx.durableExecuteAsync(awaitCall).await();
-            consumed = true;
-            if (registry != null) {
-                registry.untrackPendingSubagentCall(identity());
+            try {
+                DurableCallable<SubagentResult> awaitCall =
+                        setup.awaitResult(ctx, getSessionId(), getCallId());
+                value = ctx.durableExecuteAsync(awaitCall).await();
+                consumed = true;
+            } finally {
+                // Preserve the exception while releasing the action's 
pending-handle obligation.
+                if (registry != null) {
+                    registry.untrackPendingSubagentCall(identity());
+                }
             }
         }
         return value;
diff --git 
a/runtime/src/main/java/org/apache/flink/agents/runtime/subagent/DeferredSubagentFuture.java
 
b/runtime/src/main/java/org/apache/flink/agents/runtime/subagent/DeferredSubagentFuture.java
index a6e62fed8..64b30308b 100644
--- 
a/runtime/src/main/java/org/apache/flink/agents/runtime/subagent/DeferredSubagentFuture.java
+++ 
b/runtime/src/main/java/org/apache/flink/agents/runtime/subagent/DeferredSubagentFuture.java
@@ -76,8 +76,15 @@ public final class DeferredSubagentFuture extends 
SubagentFuture implements Phas
      */
     @Override
     public void execute() throws Exception {
-        prepare();
-        complete(ctx.durableExecuteAsync(prepared).await());
+        try {
+            prepare();
+            complete(ctx.durableExecuteAsync(prepared).await());
+        } finally {
+            // Await failures may be handled by ToolCallAction; they still 
resolve this obligation.
+            if (registry != null) {
+                registry.untrackPendingSubagentCall(identity());
+            }
+        }
     }
 
     /**
@@ -103,9 +110,6 @@ public final class DeferredSubagentFuture extends 
SubagentFuture implements Phas
     private void complete(SubagentResult outcome) {
         this.value = outcome;
         this.done = true;
-        if (registry != null) {
-            registry.untrackPendingSubagentCall(identity());
-        }
     }
 
     @Override
diff --git 
a/runtime/src/test/java/org/apache/flink/agents/runtime/subagent/SubagentAwaitCleanupTest.java
 
b/runtime/src/test/java/org/apache/flink/agents/runtime/subagent/SubagentAwaitCleanupTest.java
new file mode 100644
index 000000000..fe83e787b
--- /dev/null
+++ 
b/runtime/src/test/java/org/apache/flink/agents/runtime/subagent/SubagentAwaitCleanupTest.java
@@ -0,0 +1,247 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.agents.runtime.subagent;
+
+import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.InputEvent;
+import org.apache.flink.agents.api.OutputEvent;
+import org.apache.flink.agents.api.agents.Agent;
+import org.apache.flink.agents.api.agents.AgentExecutionOptions;
+import org.apache.flink.agents.api.chat.messages.ToolCallBlock;
+import org.apache.flink.agents.api.context.DurableCallable;
+import org.apache.flink.agents.api.context.DurableFuture;
+import org.apache.flink.agents.api.context.RunnerContext;
+import org.apache.flink.agents.api.event.ToolRequestEvent;
+import org.apache.flink.agents.api.event.ToolResponseEvent;
+import org.apache.flink.agents.api.resource.ResourceContext;
+import org.apache.flink.agents.api.resource.ResourceDescriptor;
+import org.apache.flink.agents.api.resource.ResourceType;
+import org.apache.flink.agents.api.subagent.SubagentFuture;
+import org.apache.flink.agents.api.subagent.SubagentResult;
+import org.apache.flink.agents.plan.AgentConfiguration;
+import org.apache.flink.agents.plan.AgentPlan;
+import org.apache.flink.agents.runtime.operator.ActionExecutionOperator;
+import org.apache.flink.agents.runtime.operator.ActionExecutionOperatorFactory;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.util.KeyedOneInputStreamOperatorTestHarness;
+import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CancellationException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/** Exercises handled await failures through dispatch and operator action 
finalization. */
+public class SubagentAwaitCleanupTest {
+    @ParameterizedTest
+    @CsvSource({
+        "deferred,prepare,false", "deferred,prepare,true",
+        "deferred,call,false", "deferred,call,true",
+        "async,status,false", "async,status,true",
+        "async,fetch,false", "async,fetch,true",
+        "deferred,result,false", "deferred,result,true",
+        "async,result,false", "async,result,true"
+    })
+    @Timeout(30)
+    void modelHandledFailureAllowsActionToFinish(String mode, String stage, 
boolean parallel)
+            throws Exception {
+        Agent agent = new Agent();
+        agent.addResource(
+                "child",
+                ResourceType.AGENT,
+                ResourceDescriptor.Builder.newBuilder(
+                                (mode.equals("deferred") ? DeferredSetup.class 
: AsyncSetup.class)
+                                        .getName())
+                        .addInitialArgument("stage", stage)
+                        .build());
+        agent.addAction(
+                new String[] {InputEvent.EVENT_TYPE},
+                SubagentAwaitCleanupTest.class.getMethod(
+                        "request", Event.class, RunnerContext.class));
+        agent.addAction(
+                new String[] {ToolResponseEvent.EVENT_TYPE},
+                SubagentAwaitCleanupTest.class.getMethod(
+                        "receive", Event.class, RunnerContext.class));
+        AgentConfiguration config = new AgentConfiguration();
+        config.set(AgentExecutionOptions.PARALLEL_EXECUTION_ENABLED, false);
+        config.set(AgentExecutionOptions.TOOL_CALL_ASYNC, parallel);
+        config.set(AgentExecutionOptions.ASYNC_BATCH_PARALLELISM, 2);
+        AgentPlan compiled = new AgentPlan(agent, config);
+        Map<String, org.apache.flink.agents.plan.actions.Action> actions =
+                new LinkedHashMap<>(compiled.getActions());
+        actions.remove("chat_model_action");
+        AgentPlan plan = new AgentPlan(actions, 
compiled.getResourceProviders(), config);
+        try (KeyedOneInputStreamOperatorTestHarness<Long, Long, Object> 
harness =
+                new KeyedOneInputStreamOperatorTestHarness<>(
+                        new ActionExecutionOperatorFactory<Long, Object>(plan, 
true),
+                        (KeySelector<Long, Long>) value -> 0L,
+                        TypeInformation.of(Long.class))) {
+            harness.open();
+            harness.processElement(new StreamRecord<>(1L));
+            ((ActionExecutionOperator<?, ?>) 
harness.getOperator()).waitInFlightEventsFinished();
+            assertThat(harness.getRecordOutput())
+                    .extracting(StreamRecord::getValue)
+                    .containsExactly("handled");
+        }
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+        "deferred,error",
+        "deferred,cancel",
+        "deferred,interrupt",
+        "async,error",
+        "async,cancel",
+        "async,interrupt"
+    })
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    void directAwaitPreservesFailureAndReleasesHandle(String mode, String 
failureKind)
+            throws Exception {
+        Exception failure =
+                failureKind.equals("cancel")
+                        ? new CancellationException("await failed")
+                        : failureKind.equals("interrupt")
+                                ? new InterruptedException("await failed")
+                                : new IllegalStateException("await failed");
+        RunnerContext ctx = mock(RunnerContext.class);
+        when(ctx.durableExecuteAsync(any(DurableCallable.class)))
+                .thenReturn(
+                        (DurableFuture)
+                                () -> {
+                                    throw failure;
+                                });
+        PendingSubagentCallRegistry registry = new 
PendingSubagentCallRegistry("caller");
+        ResourceDescriptor descriptor =
+                ResourceDescriptor.Builder.newBuilder(
+                                (mode.equals("deferred") ? DeferredSetup.class 
: AsyncSetup.class)
+                                        .getName())
+                        .addInitialArgument("stage", "result")
+                        .build();
+        SubagentFuture future =
+                mode.equals("deferred")
+                        ? new DeferredSubagentFuture(
+                                "s",
+                                "c",
+                                ctx,
+                                registry,
+                                () ->
+                                        new DeferredSetup(descriptor, null)
+                                                .prepare(ctx, "hello", "s", 
"c"))
+                        : new AsyncSubagentFuture(
+                                new AsyncSetup(descriptor, null), ctx, "s", 
"c", registry);
+        assertThatThrownBy(future::await).isSameAs(failure);
+        registry.checkEmpty();
+    }
+
+    public static void request(Event event, RunnerContext ctx) {
+        ctx.sendEvent(
+                new ToolRequestEvent(
+                        "model",
+                        List.of(
+                                new ToolCallBlock(
+                                        "call-1", "_subagent_child", 
Map.of("question", "hello")),
+                                new ToolCallBlock(
+                                        "call-2",
+                                        "_subagent_child",
+                                        Map.of("question", "hello")))));
+    }
+
+    public static void receive(Event event, RunnerContext ctx) {
+        ToolResponseEvent response = ToolResponseEvent.fromEvent(event);
+        for (String id : List.of("call-1", "call-2")) {
+            assertThat(response.getSuccess().get(id)).isFalse();
+            assertThat(response.getError().get(id)).contains("remote 
unavailable");
+        }
+        ctx.sendEvent(new OutputEvent("handled"));
+    }
+
+    public static class DeferredSetup extends BaseDeferredSubagentSetup {
+        private final String stage;
+
+        public DeferredSetup(ResourceDescriptor descriptor, ResourceContext 
context) {
+            super(descriptor, context);
+            stage = descriptor.getArgument("stage");
+        }
+
+        @Override
+        protected DurableCallable<SubagentResult> prepare(
+                RunnerContext ctx, Object prompt, String sessionId, String 
callId) {
+            if (stage.equals("prepare")) {
+                throw new IllegalStateException("remote unavailable");
+            }
+            return new DurableCallable<SubagentResult>() {
+                @Override
+                public String getId() {
+                    return sessionId + "#" + callId;
+                }
+
+                @Override
+                public Class<SubagentResult> getResultClass() {
+                    return SubagentResult.class;
+                }
+
+                @Override
+                public SubagentResult call() {
+                    if (stage.equals("call")) {
+                        throw new IllegalStateException("remote unavailable");
+                    }
+                    return SubagentResult.error("remote unavailable");
+                }
+            };
+        }
+    }
+
+    public static class AsyncSetup extends BaseAsyncSubagentSetup {
+        private final String stage;
+
+        public AsyncSetup(ResourceDescriptor descriptor, ResourceContext 
context) {
+            super(descriptor, context);
+            stage = descriptor.getArgument("stage");
+        }
+
+        @Override
+        protected void callSubmitRequest(String sessionId, String callId, 
Object prompt) {}
+
+        @Override
+        protected RunStatus callQueryStatus(String sessionId, String callId) {
+            if (stage.equals("status")) {
+                throw new IllegalStateException("remote unavailable");
+            }
+            return RunStatus.completed();
+        }
+
+        @Override
+        protected SubagentResult callFetchResult(String sessionId, String 
callId) {
+            if (stage.equals("fetch")) {
+                throw new IllegalStateException("remote unavailable");
+            }
+            return SubagentResult.error("remote unavailable");
+        }
+    }
+}

Reply via email to