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

commit bff15845ff29069416f9aa7465434b11611230b5
Author: WenjinXie <[email protected]>
AuthorDate: Tue Oct 20 11:14:35 2026 +0800

    [docs] Clarify sub-agent usage and execution semantics
    
    Reorganize the sub-agent guide around declaration, invocation and result 
handling. Clarify schemas, external setup modes, session ownership, recovery 
guarantees, observability and usage limits.
    
    Generated-by: Codex CLI 0.153.4 (GPT-6)
    Co-authored-by: Codex <[email protected]>
---
 .../docs/development/multi-agent/subagents.md      | 737 +++++++++++----------
 1 file changed, 397 insertions(+), 340 deletions(-)

diff --git a/docs/content/docs/development/multi-agent/subagents.md 
b/docs/content/docs/development/multi-agent/subagents.md
index aa3e2cd13..0472561db 100644
--- a/docs/content/docs/development/multi-agent/subagents.md
+++ b/docs/content/docs/development/multi-agent/subagents.md
@@ -31,58 +31,53 @@ input, the sub-agent does the work, and the parent receives 
the outcome as a
 `SubagentResult`. Use one when the delegated work is multi-step or 
long-running and
 should run isolated from the parent.
 
-The work runs either inside the job or outside it. An *internal* sub-agent is a
-child `Agent`, including a `ReActAgent`, compiled into the parent's plan. The
-child's own actions and events run in the same job. An *external* sub-agent 
has no
-plan of its own. The work happens in your own code or in a remote agent 
service,
-outside the parent's event loop. It takes one of two forms, *deferred* or 
*async
-(pub/sub)*.
-
-Either way, the call is initiated by the model or by you. In *model-driven
-delegation* you list the sub-agent on a chat model setup and the model chooses 
when
-to delegate. In *caller-driven delegation* one of your actions submits the task
-itself.
+There are two kinds of sub-agents:
+
+- **Internal**: a child `Agent`, such as a workflow agent or a `ReActAgent`. 
It runs
+  in the same Flink job as the parent, with its own actions and events.
+- **External**: custom code or a remote agent service that performs the 
delegated
+  task. You provide a setup class that connects the parent to that 
implementation.
+
+Both kinds support two ways to delegate a task:
+
+- **Model-driven**: bind the sub-agents to a chat model. The model decides 
whether
+  to delegate, chooses a sub-agent, and
+  supplies its input. The framework invokes the sub-agent and returns the 
outcome
+  to the model as a tool result, so the model can use it in its next response.
+- **Caller-driven**: call the sub-agent explicitly from one of the parent's 
actions.
+  Your code chooses the sub-agent, submits the input, and awaits the result. 
The
+  result returns to your action, which decides what to do next. Use this when 
your
+  workflow determines when to delegate.
 
 Every sub-agent call runs through
 [durable execution]({{< ref 
"docs/development/create-agent/actions#durable-execution" >}}).
 
 ## Declare a sub-agent
 
-A sub-agent enters the parent's plan as a resource registered under
-`ResourceType.AGENT`. The name you register it under is the name that 
invocations and
-metrics use. What you register depends on the form. An internal sub-agent 
registers
-a child `Agent`. An external sub-agent registers a descriptor that names your 
setup
-class. A YAML file declares either form, as
-[Declaring sub-agents]({{< ref 
"docs/development/create-agent/yaml#declaring-sub-agents" >}})
-describes. See [Resources]({{< ref "docs/development/create-agent/resources" 
>}})
-for the common registration and access model.
+Register a sub-agent on the parent under `ResourceType.AGENT` and give it a 
name.
+Use that name when calling the sub-agent from an action or binding it to a 
chat model.
+The following sections show how to register internal and external sub-agents.
 
-### Internal: compile a child agent {#declare-an-internal-sub-agent}
+Both kinds can also be declared in YAML. See
+[Declaring sub-agents]({{< ref 
"docs/development/create-agent/yaml#declaring-sub-agents" >}})
+for YAML examples and [Resources]({{< ref 
"docs/development/create-agent/resources" >}})
+for resource registration and access.
 
-Nothing in a child's own code marks it as a sub-agent. The child is an ordinary
-agent with its own resources, actions, and event loop, and it runs standalone
-exactly as written. A parent registers it as an `AGENT` resource, and that
-registration is the whole of the difference. It gives the child a name in the
-parent's plan. It can also give the child an input schema, so a chat model can 
call
-it. And it supplies the caller that submits input and awaits the child's 
outputs.
+### Internal: use an existing agent {#declare-an-internal-sub-agent}
 
-Any `Agent` can serve as the child. The two common kinds are a workflow agent 
and a
-`ReActAgent`. Either way, the child's plan is compiled into the parent's when 
the
-parent's plan is built.
+An internal sub-agent is an ordinary `Agent`, such as a workflow agent or a
+`ReActAgent`, with its own resources and actions. Register it on the parent as 
an
+`AGENT` resource so the parent can submit tasks to it and receive its results.
 
 #### A workflow agent as the child
 
-Any workflow agent can serve as the child. The one used through this page is 
built
-the way
-[Using Chat Events]({{< ref "docs/development/calling-llms/chat#usage-example" 
>}})
-builds an agent that owns a chat model, and this page calls it `ReviewAgent`. 
It has
-a connection resource, a model setup, an action that turns an `InputEvent` 
into a
-`ChatRequestEvent`, and an action that turns the `ChatResponseEvent` into
-`OutputEvent`s.
+The example below uses a workflow agent named `ReviewAgent` to review a code 
diff
+and return feedback. For how to build an agent that calls a chat model, see
+[Using Chat Events]({{< ref "docs/development/calling-llms/chat#usage-example" 
>}}).
 
-The one part a sub-agent changes is how that first action reads its input. A
-caller-driven call passes the object the caller submitted. A model-driven call
-passes the arguments map built from the input schema.
+Its input-handling action supports both delegation styles. When called from an
+action, it receives the submitted input directly. When called by a model, it
+receives an arguments map whose fields are defined by the input schema.
 
 {{< tabs "Read the Input in a Child Action" >}}
 
@@ -130,28 +125,27 @@ public static void ask(Event event, RunnerContext ctx) 
throws Exception {
 
 {{< /tabs >}}
 
-While a call runs, the child behaves like a scoped agent of its own:
+When you call an internal sub-agent:
 
-- Its `OutputEvent`s are accumulated, in order, into the call's result instead 
of
-  landing on the parent's context. See
-  [the result](#the-result) for the shape.
+- The parent receives the child's outputs as the call's result, in emission 
order.
+  See [the result](#the-result) for the result structure.
 - The child first looks for resources registered on itself, then falls back to
   resources registered on the root agent. You can share a chat model connection
   by registering it on the root, or override it on a child using the same name
   and resource type. This also applies to nested sub-agents.
-- Its sensory and short-term memory start empty for each call, including calls
-  continuing a session, and are shared by the child's actions within that call.
-  Long-term memory, when configured, is shared with the caller.
-- It may register and await sub-agents of its own, so nesting is unbounded in
-  depth.
+- The child's sensory and short-term memory start empty for each call, even 
when
+  reusing a session ID. Its actions share this memory during the call. 
Long-term
+  memory, when configured, is shared with the caller.
+- The child can also register and call sub-agents of its own.
+
+Choose the registration form based on how the child will be called:
 
-Registering the child takes one of two forms, and which one you need follows 
from who
-calls it, the model or one of your actions. A model can delegate only to a 
sub-agent
-it can describe and build arguments for, so a child exposed to a model needs 
the
-descriptor form. A child that only your actions call can register as-is, and 
that
-registration wraps it in a descriptor carrying nothing else.
+- **Model-driven**: use the builder shown below to register the child with an 
input
+  type or schema. Add a description to help the model decide when to use it.
+- **Caller-driven only**: you can register the `Agent` directly. A description 
and
+  input declaration are optional.
 
-Which parameters the descriptor must carry depends on the same thing.
+When using the builder, the parameters are:
 
 | Parameter | Model-driven calls | Caller-driven calls |
 |---|---|---|
@@ -201,8 +195,9 @@ parent.addResource(
 
 {{< /tabs >}}
 
-`ReviewRequest` describes the arguments the child accepts, and the Java form 
is a bean.
-Where no type fits, a JSON schema string declares the same arguments:
+`input_type` defines the arguments a model supplies when calling the sub-agent,
+using a Pydantic model in Python or a Java bean. You can also use 
`input_schema`
+to describe the same arguments directly with JSON Schema:
 
 {{< tabs "Declare the Input as a Schema" >}}
 
@@ -240,16 +235,12 @@ parent.addResource(
 
 {{< /tabs >}}
 
-Declare either an input type or an input schema, not both. A declaration that 
carries
-both is rejected when the descriptor is built. In Python the type must be a 
pydantic
-model, and a type that is not is rejected there too.
-
-An input type is a shorthand for a schema. The framework derives the schema 
from the
-type and registers that, so use a type when you already have one and a schema 
when
-you do not. Nothing is ever converted into an instance of the type. The 
sub-agent
-receives the arguments as a map keyed by the schema's field names.
+Choose either `input_type` or `input_schema`. With either form, a model-driven
+call passes the arguments to the child as a Python dictionary or a Java `Map`.
+For example, declaring `ReviewRequest` above means the child receives
+`{"code": "..."}`, not a `ReviewRequest` instance.
 
-For calls an action makes itself, the child registers as-is:
+If you only call the sub-agent from actions, you can also register the agent 
directly:
 
 {{< tabs "Register the Child Agent Directly" >}}
 
@@ -270,9 +261,11 @@ parent.addResource("reviewer", ResourceType.AGENT, new 
ReviewAgent());
 This form carries no description and no input declaration, so a chat model 
rejects
 the sub-agent at setup time rather than calling it with no arguments.
 
-A YAML file names the child instead of instantiating it. Its entry carries an
-`agent` field holding the name of another Agent in the same document, along 
with the
-description and the input schema.
+In YAML, use the `agent` field to reference a child agent by name. The child 
can
+be defined in the same file or loaded earlier into the same execution 
environment.
+You can also provide a description and an input schema. See
+[Internal sub-agents in YAML]({{< ref 
"docs/development/create-agent/yaml#internal-sub-agents" >}})
+for examples and loading order.
 
 #### A ReActAgent as the child
 
@@ -353,24 +346,26 @@ parent.addResource(
 
 {{< /tabs >}}
 
-With a prompt, each schema field fills the placeholder of the same name. See
-[Prompt]({{< ref "docs/development/react_agent#prompt" >}}). A string input 
from an
-action fills a placeholder named `input`. Declare a field for every 
placeholder the
-prompt uses. Both examples reuse the connection the previous example registers 
on the
-child:
+When the child has a prompt, define an input schema whose fields match the 
prompt's
+placeholders. In the example below, the prompt contains `{diff}`, so the schema
+declares a `diff` field. Pass `{"diff": "..."}` when calling this child.
+
+If an action passes a plain string instead of an argument map, the string 
fills a
+placeholder named `{input}`. Use `{input}` in the child's prompt for this form 
of
+input. See [Prompt]({{< ref "docs/development/react_agent#prompt" >}}) for 
details.
+
+Connection configuration is unchanged from the previous example and is omitted 
below.
 
 {{< tabs "Use a ReActAgent With a Prompt" >}}
 
 {{< tab "Python" >}}
 ```python
-chat_model_descriptor = ResourceDescriptor(
-    clazz=ResourceName.ChatModel.OLLAMA_SETUP,
-    connection="reviewer_connection",
-    model="qwen3:8b",
-)
-
 reviewer = ReActAgent(
-    chat_model=chat_model_descriptor,
+    chat_model=ResourceDescriptor(
+        clazz=ResourceName.ChatModel.OLLAMA_SETUP,
+        connection="reviewer_connection",
+        model="qwen3:8b",
+    ),
     prompt=Prompt.from_text(
         "Review the following code diff and report any problems:\n\n{diff}"
     ),
@@ -394,15 +389,12 @@ parent.add_resource(
 
 {{< tab "Java" >}}
 ```java
-ResourceDescriptor chatModelDescriptor =
-        
ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.OLLAMA_SETUP)
-                .addInitialArgument("connection", "reviewerConnection")
-                .addInitialArgument("model", "qwen3:8b")
-                .build();
-
 ReActAgent reviewer =
         new ReActAgent(
-                chatModelDescriptor,
+                
ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.OLLAMA_SETUP)
+                        .addInitialArgument("connection", "reviewerConnection")
+                        .addInitialArgument("model", "qwen3:8b")
+                        .build(),
                 Prompt.fromText(
                         "Review the following code diff and report any 
problems:\n\n{diff}"),
                 null);
@@ -429,24 +421,12 @@ text. So keep the two in step when you change either.
 
 ### External: write a setup class {#declare-an-external-sub-agent}
 
-An external sub-agent has no plan of its own, and it is not compiled into the
-parent's. The work happens in your own code or in a remote agent service. The 
parent
-only submits to it and waits for the outcome. The framework cannot perform 
that work
-for you, because it does not know your protocol. So you write the part that 
performs
-one invocation, and the framework supplies everything around it. It runs your 
code
-under durable execution, and after a failover it replays the recorded result 
instead
-of calling your code again.
+An external sub-agent delegates work to your own code or a remote agent 
service.
+To use an external sub-agent, extend `DeferredSubagentSetup`
+(`BaseDeferredSubagentSetup` in Java) or `BaseAsyncSubagentSetup`, implement 
the
+methods for calling your code or service, and register it on the parent agent.
 
-What you write is a setup class. Its only job is to carry out one invocation, 
so it
-declares no actions and no events of its own. It extends a framework base 
class and
-implements the methods that class declares.
-
-Registration names the setup class in a descriptor, and both languages 
construct the
-setup from the descriptor at runtime. Registering a setup instance instead is
-rejected.
-
-The invocation itself is carried out in one of two modes, *deferred* and *async
-(pub/sub)*. Which one you write follows from your remote interface.
+Choose between *deferred* and *async (pub/sub)* based on the interface you are 
calling:
 
 | | Deferred | Async (pub/sub) |
 |---|---|---|
@@ -456,76 +436,18 @@ The invocation itself is carried out in one of two modes, 
*deferred* and *async
 
 #### Deferred execution
 
-A deferred setup sends nothing when the caller submits. It only registers the
-invocation. The request goes out when the caller first awaits the result. So a 
call
-that is cancelled instead never issues its request.
-
-What you write is the interaction itself, in the `prepare` method of your 
setup class.
-`prepare` runs once per invocation and returns the durable call that carries 
it. The
-example is one blocking request, and whatever else your call does is yours to 
decide,
-because the framework does not know your protocol.
-
-The framework assigns `session_id` and `call_id` before it calls `prepare`, 
and they
-are the only values that a replay reproduces. The durable id you build from 
them is
-the key that recovery matches a recorded result against. When the key matches, 
your
-call is not run again. So build the key from those two values alone. Anything 
else in
-it, a timestamp for instance, makes a replay look up a different key and run 
the work
-a second time. Passing the same values to your own remote lets it recognize a
-re-issued request as the same one.
-
-The reconciler is optional. Omitting it leaves one crash window uncovered, and
-[Durability and failover](#durability-and-failover) describes that window. 
Supply one
-when the work cannot be repeated.
-
-{{< tabs "Supply a Reconciler" >}}
-
-{{< tab "Python" >}}
-```python
-def prepare(
-    self, ctx: RunnerContext, prompt: Any, session_id: str, call_id: str
-) -> PreparedTriple:
-    key = f"{session_id}#{call_id}"
-
-    def call() -> SubagentResult:
-        return SubagentResult.ok(post_review(self.endpoint, prompt, key))
-
-    # Consulted only during recovery, when this call is revisited and no 
result was
-    # recorded for it. It takes no arguments. Return the recovered outcome, 
because
-    # an exception here is a system-level failure that fails the action 
instead of
-    # reaching the caller.
-    def reconcile() -> SubagentResult:
-        # fetch_review_by_key is your own lookup; not part of Flink Agents.
-        found = fetch_review_by_key(self.endpoint, key)
-        if found is None:
-            return SubagentResult.error("the review service has no record of 
this call")
-        return SubagentResult.ok(found)
-
-    return (key, call, reconcile)
-```
-{{< /tab >}}
+A deferred sub-agent starts its request when the caller first awaits the 
result.
+Submitting the call alone does not send a request. Cancelling it before the 
first
+await therefore prevents the request from being sent.
 
-{{< tab "Java" >}}
-```java
-// Inside the DurableCallable that prepare returns, alongside getId() and 
call().
-
-// Consulted only during recovery, when this call is revisited and no result 
was
-// recorded for it. Return the recovered outcome, because an exception here is 
a
-// system-level failure that fails the action instead of reaching the caller.
-@Override
-public Callable<SubagentResult> reconciler() {
-    return () -> {
-        // fetchReviewByKey is your own lookup; not part of Flink Agents.
-        String found = fetchReviewByKey(endpoint, key);
-        if (found == null) {
-            return SubagentResult.error("the review service has no record of 
this call");
-        }
-        return SubagentResult.ok(found);
-    };
-}
-```
-{{< /tab >}}
+Implement `prepare` to define the function that sends the request and returns a
+`SubagentResult`. In Python, return `(key, call, reconcile)`; in Java, return a
+`DurableCallable<SubagentResult>`. The example below performs one request and 
leaves
+out the optional reconciler.
 
-{{< /tabs >}}
+Build the call's key from the supplied `session_id` and `call_id`, as shown 
below,
+so it stays the same during recovery. You can also pass this key to your 
service
+so it can recognize repeated requests for the same work.
 
 {{< tabs "Deferred Sub-agent Setup" >}}
 
@@ -630,9 +552,12 @@ public class RemoteReviewSetup extends 
BaseDeferredSubagentSetup {
 
 {{< /tabs >}}
 
-Register the setup under the name invocations will use. Every descriptor 
argument
-beyond the built-in ones is configuration for the setup to read back. In 
Python it
-reads them as model fields, and in Java through the public
+Register the external sub-agent using a `ResourceDescriptor` that names its 
setup
+class. Both Java and Python require this form; passing a setup instance 
directly
+is not supported. Give the resource the name the parent will use to call it.
+
+Every descriptor argument beyond the built-in ones is configuration for the 
setup
+to read back. In Python it reads them as model fields, and in Java through the 
public
 `(ResourceDescriptor, ResourceContext)` constructor. The `endpoint` above is 
such
 configuration, not a built-in transport.
 
@@ -701,28 +626,92 @@ Two constraints apply to the entry:
 - The entry's language must match the loader. A Python YAML file names a Python
   class and may not set `type: java`. A Java YAML file must set `type: java`.
 
+<a id="optional-reconciler"></a>
+
+**Optional reconciler**
+
+If repeating a request could duplicate its effects, add a reconciler that 
checks
+your service for the existing outcome. During recovery, the framework uses this
+lookup when no result was recorded for the call. See
+[Durability and failover](#durability-and-failover) for how this handles a 
failure
+after the service has completed the work but before the result is persisted.
+
+The following code adds a reconciler to the setup above:
+
+{{< tabs "Supply a Reconciler" >}}
+
+{{< tab "Python" >}}
+```python
+def prepare(
+    self, ctx: RunnerContext, prompt: Any, session_id: str, call_id: str
+) -> PreparedTriple:
+    key = f"{session_id}#{call_id}"
+
+    def call() -> SubagentResult:
+        return SubagentResult.ok(post_review(self.endpoint, prompt, key))
+
+    # Consulted only during recovery, when this call is revisited and no 
result was
+    # recorded for it. It takes no arguments. Return the recovered outcome, 
because
+    # an exception here is a system-level failure that fails the action 
instead of
+    # reaching the caller.
+    def reconcile() -> SubagentResult:
+        # fetch_review_by_key is your own lookup; not part of Flink Agents.
+        found = fetch_review_by_key(self.endpoint, key)
+        if found is None:
+            return SubagentResult.error("the review service has no record of 
this call")
+        return SubagentResult.ok(found)
+
+    return (key, call, reconcile)
+```
+{{< /tab >}}
+
+{{< tab "Java" >}}
+```java
+// Inside the DurableCallable that prepare returns, alongside getId() and 
call().
+
+// Consulted only during recovery, when this call is revisited and no result 
was
+// recorded for it. Return the recovered outcome, because an exception here is 
a
+// system-level failure that fails the action instead of reaching the caller.
+@Override
+public Callable<SubagentResult> reconciler() {
+    return () -> {
+        // fetchReviewByKey is your own lookup; not part of Flink Agents.
+        String found = fetchReviewByKey(endpoint, key);
+        if (found == null) {
+            return SubagentResult.error("the review service has no record of 
this call");
+        }
+        return SubagentResult.ok(found);
+    };
+}
+```
+{{< /tab >}}
+
+{{< /tabs >}}
+
 #### Async (pub/sub) execution
 
-An async sub-agent talks to a service that runs the task for you. The service 
accepts
-the work and answers at once, and you query it later for the run's status and
-result.
+Use an async sub-agent when a service accepts a task and completes it later.
+The call has three steps:
 
-An async setup starts the remote run when the caller submits, through one 
durable
-POST. Waiting for the result then polls the run's status until it reaches a 
terminal
-state, and fetches it.
+1. **Submit the task.** When the parent submits the call, send a request to 
start
+   the task on the remote service.
+2. **Query its status.** When the caller awaits the result, the framework 
repeatedly
+   checks whether the task has completed or failed.
+3. **Fetch the result.** Once the task completes, retrieve its result. If the 
task
+   fails, the caller receives an error result without a separate fetch.
 
-The base class owns that submit-poll-fetch composition, along with the durable
-execution and the crash-window recovery. You supply the three transport calls 
it is
-built from:
+Extend `BaseAsyncSubagentSetup` and implement these methods for your service:
 
-- `callSubmitRequest` / `call_submit_request` starts the run.
-- `callQueryStatus` / `call_query_status` returns a `RunStatus`, one of
-  `notStarted()`, `running()`, `completed()`, or `failed(error)`. It must be a
-  read-only probe with no side effects. Report an expected failure as a 
`FAILED`
-  status.
-- `callFetchResult` / `call_fetch_result` returns the `SubagentResult`. It 
must be
-  an idempotent read, because a replay may call it again. The status snapshot 
never
-  carries the payload, and a failed run is reported without fetching at all.
+| Step | Python | Java |
+|---|---|---|
+| Submit | `call_submit_request` | `callSubmitRequest` |
+| Query status | `call_query_status` | `callQueryStatus` |
+| Fetch result | `call_fetch_result` | `callFetchResult` |
+
+Each method receives `session_id` and `call_id` (`sessionId` and `callId` in 
Java)
+to identify the same task. The example below combines them into a task ID used 
by
+the service. The status method returns a `RunStatus`, and the fetch method 
returns
+a `SubagentResult`. Report an expected task failure as a `FAILED` status.
 
 {{< tabs "Async Sub-agent Setup" >}}
 
@@ -733,8 +722,8 @@ from typing import Any
 from flink_agents.api.subagent import SubagentResult
 from flink_agents.runtime.async_subagent import BaseAsyncSubagentSetup, 
RunStatus
 
-# client(...) and task_id stand for your own transport client and run-id
-# scheme; neither is part of Flink Agents.
+# client(...) is your service client; task_id is how your service identifies 
tasks.
+# Neither is part of Flink Agents.
 
 
 class RemoteRunSetup(BaseAsyncSubagentSetup):
@@ -757,12 +746,6 @@ class RemoteRunSetup(BaseAsyncSubagentSetup):
         return SubagentResult.ok(
             client(self.endpoint).fetch(task_id=f"{session_id}#{call_id}")
         )
-
-    # Optional, and a no-op by default, so a cancelled call leaves the remote 
run
-    # going. A replay may propagate the same cancellation again, so your 
remote has
-    # to accept it twice.
-    def call_cancel_request(self, session_id: str, call_id: str) -> None:
-        client(self.endpoint).cancel(task_id=f"{session_id}#{call_id}")
 ```
 {{< /tab >}}
 
@@ -773,8 +756,8 @@ import 
org.apache.flink.agents.api.resource.ResourceDescriptor;
 import org.apache.flink.agents.api.subagent.SubagentResult;
 import org.apache.flink.agents.runtime.subagent.BaseAsyncSubagentSetup;
 
-// client() and taskId(...) stand for your own transport client and run-id
-// scheme; neither is part of Flink Agents.
+// client() is your service client; taskId(...) builds the service's task ID.
+// Neither is part of Flink Agents.
 public class RemoteRunSetup extends BaseAsyncSubagentSetup {
 
     private final String endpoint;
@@ -792,7 +775,7 @@ public class RemoteRunSetup extends BaseAsyncSubagentSetup {
 
     @Override
     protected RunStatus callQueryStatus(String sessionId, String callId) {
-        // TaskStatus is your own transport type, not a framework type.
+        // TaskStatus is the status type returned by your service client.
         TaskStatus probe = client().status(taskId(sessionId, callId));
         if (probe == null) {
             return RunStatus.notStarted();
@@ -810,14 +793,6 @@ public class RemoteRunSetup extends BaseAsyncSubagentSetup 
{
     protected SubagentResult callFetchResult(String sessionId, String callId) 
throws Exception {
         return client().fetchResult(taskId(sessionId, callId));
     }
-
-    // Optional, and a no-op by default, so a cancelled call leaves the remote 
run
-    // going. A replay may propagate the same cancellation again, so your 
remote has
-    // to accept it twice.
-    @Override
-    protected void callCancelRequest(String sessionId, String callId) {
-        client().cancel(taskId(sessionId, callId));
-    }
 }
 ```
 {{< /tab >}}
@@ -881,29 +856,53 @@ agents:
 
 {{< /tabs >}}
 
-The base class also recovers the crash window between "POST sent" and "POST 
durably
-recorded". On recovery it probes the run's status first, and it re-posts only 
when the
-probe reports that the run never started. Override `reconcileSubmitRequest` /
-`reconcile_submit_request` only if your service needs a different repair.
+<a id="recovery-and-cancellation"></a>
 
-## Invoke a sub-agent
+**Recovery and cancellation**
+
+If a failure occurs after the submission request was sent but before its 
completion
+was recorded, recovery first queries the task's status. The framework 
resubmits the
+request only if the service reports that the task has not started. Override
+`reconcile_submit_request` (`reconcileSubmitRequest` in Java) if your service 
needs
+a different recovery procedure. See [Durability and 
failover](#durability-and-failover)
+for more details.
+
+Status queries must be read-only. Fetching the result must also be safe to 
repeat,
+since recovery may fetch it again.
+
+To cancel the remote task when the caller cancels the call, implement
+`call_cancel_request` (`callCancelRequest` in Java) in your setup class. 
Without this
+optional method, cancelling the call leaves the remote task running. The 
service
+must accept repeated cancellation requests, since recovery may send them again.
 
-A sub-agent is invoked in one of two ways, model-driven or caller-driven. The
-difference you see is where the result goes.
+{{< tabs "Cancel an Async Sub-agent Task" >}}
+
+{{< tab "Python" >}}
+```python
+# Add this method to RemoteRunSetup.
+def call_cancel_request(self, session_id: str, call_id: str) -> None:
+    client(self.endpoint).cancel(task_id=f"{session_id}#{call_id}")
+```
+{{< /tab >}}
+
+{{< tab "Java" >}}
+```java
+// Add this method to RemoteRunSetup.
+@Override
+protected void callCancelRequest(String sessionId, String callId) {
+    client().cancel(taskId(sessionId, callId));
+}
+```
+{{< /tab >}}
 
-In model-driven delegation the result returns to the model. The sub-agent is 
offered
-to the model as a tool, so your action sends a `ChatRequestEvent` and the model
-answers with a call to the sub-agent. The result goes back to the model as the 
tool
-observation, and the model reasons over it until a final `ChatResponseEvent` 
reaches
-your action.
+{{< /tabs >}}
 
-In caller-driven delegation the result returns to your action. An action that 
needs
-the delegated outcome calls the sub-agent itself, with no model in the loop.
+## Invoke a sub-agent
 
-Both ways end in the same call. Your action, or the framework on the model's 
behalf,
-runs `submit(ctx, prompt)` on the registered name. That call returns a
-`SubagentFuture`, which is the handle for that one call. Awaiting the handle 
yields
-the call's `SubagentResult`.
+- **Model-driven**: the model chooses and calls a sub-agent, then uses the 
result
+  to continue processing the task.
+- **Caller-driven**: an action explicitly calls a sub-agent, awaits its result,
+  and decides how to use it.
 
 ### Delegate from a chat model
 
@@ -938,28 +937,22 @@ The model sees `reviewer` as a tool, described by the 
registration's description
 input schema. A schema is required for model-selected calls. A missing 
description
 falls back to a generic delegation description.
 
-The arguments the model sends become the sub-agent's input, and its outputs 
return to
-the model as the tool observation. The model can then keep reasoning over 
them. For
-an internal sub-agent, the observation is the JSON encoding of the full output 
list
-described in [the result](#the-result). That holds even when the child emits a 
single
-output, so the shape does not depend on how many outputs the child emits.
-
-An external setup class can state the shape of that observation. It declares
-`result_type()` in Python or `getResultType()` in Java, and the default 
reports the
-result as it arrived. Declare one to narrow a wider payload, or to convert a 
payload
-that is not already JSON-compatible. An internal sub-agent has no such 
declaration, so
-its observation is always the JSON encoding of its output list. This shapes the
-observation only. A caller that awaits the handle gets the result the sub-agent
-produced, as [the result](#the-result) describes.
+The model's arguments become the sub-agent's input. Its result returns to the 
model
+as a tool observation, which the model can use to continue processing the task.
+See [The result](#the-result) for the result format.
 
 The external sub-agents `remote_reviewer` and `remote_runner` are offered the 
same
 way. Only their registration differs.
 
 ### Call from an action
 
-An action can submit input to a registered sub-agent directly and await its
-outcome. This path needs no description and no input declaration, so it also 
works
-for a child registered as-is.
+An action can call a registered sub-agent directly, without declaring a 
description
+or input schema. Pass the input to `submit(ctx, prompt)` to obtain a 
`SubagentFuture`,
+then await that future to get a `SubagentResult`.
+
+In Python, `submit` is a coroutine: use `await reviewer.submit(...)` to obtain 
the
+future, then `await future` for the result. The action must therefore be 
`async`.
+In Java, `submit` returns the future directly, and `await()` returns its 
result.
 
 {{< tabs "Call a Sub-agent From an Action" >}}
 
@@ -1001,17 +994,10 @@ public class DelegatingAgent extends Agent {
 
 {{< /tabs >}}
 
-What you pass to `submit` is what the sub-agent receives. An internal child 
gets it
-as its `InputEvent` payload, and a model-driven call passes the arguments map
-instead. What comes back is a `SubagentResult`. For the internal sub-agent 
used here,
-its `result` is the list of `OutputEvent` payloads the child emitted. That is 
why the
-example reads `result.result[0]`. An external sub-agent returns whatever its 
setup
-produces.
-
-In Python `submit` is a coroutine, so the call is awaited in two phases and the
-action must be `async`. In Java `submit` returns the handle directly and 
`await()`
-yields the result. The same two calls drive `remote_reviewer` and 
`remote_runner`
-unchanged. Only the registration differs.
+An internal child receives the submitted input as its `InputEvent` payload. The
+example reads the first output from the internal `reviewer`; see
+[The result](#the-result) for how to read internal and external sub-agent 
results.
+Use the same `submit` and await methods to call `remote_reviewer` or 
`remote_runner`.
 
 ## Manage sub-agent calls
 
@@ -1022,12 +1008,19 @@ your own records.
 
 ### Fan out to several sub-agents
 
-`combine` groups handles into a `SubagentFutures`, and resolving the batch 
resolves
-every handle in it. `gather` does not accept a `SubagentFuture`, so handles 
batch this
-way instead. The batch prepares every handle before it runs any of them. 
Preparing an
-internal child starts that child, so the children's work can overlap. An 
external
-deferred setup issues its request only when the batch runs it, so those 
requests do
-not overlap. The results come back in the order the handles were added.
+Use `combine` to wait for several sub-agent calls together. Results are 
returned in
+the order the calls were added, regardless of completion order. Whether their 
work
+can overlap depends on the sub-agent type:
+
+| Sub-agent type | Execution behavior |
+|---|---|
+| Internal | Child agents can run concurrently. |
+| External deferred | Requests run one at a time; `combine` does not make them 
concurrent. |
+| External async | Tasks start when submitted and can run concurrently if the 
remote service supports it. |
+
+In Python, use `combine` to await `SubagentFuture` objects together.
+`ctx.gather` does not accept these objects, and `asyncio.gather` is not 
supported
+in agent actions.
 
 {{< tabs "Fan Out to Several Sub-agents" >}}
 
@@ -1057,38 +1050,49 @@ List<SubagentResult> outcomes = 
first.combine(second).awaitAll();
 
 ### Continue a session
 
-Pass the `session_id` of an earlier handle to continue that conversation. The 
new
-call gets a fresh `call_id` in the same session. Whether a conversation can be
-continued across actions is up to the implementation.
+An external sub-agent can continue a conversation if its remote service 
supports
+sessions. Its setup must map `session_id` to a remote session and use that 
session
+for subsequent requests. The conversation context is stored by the remote 
service.
+
+The following example assumes `session_reviewer` is an external sub-agent 
registered
+with such a setup. Pass the first call's `session_id` to continue the 
conversation;
+the second call gets a new `call_id` while using the same remote session.
 
 {{< tabs "Continue a Session" >}}
 
 {{< tab "Python" >}}
 ```python
-reviewer = ctx.get_resource("reviewer", ResourceType.AGENT)
+reviewer = ctx.get_resource("session_reviewer", ResourceType.AGENT)
 
-first = await reviewer.submit(ctx, "review the diff")
+first = await reviewer.submit(ctx, "Review this diff: ...")
 first_result = await first
 
-second = await reviewer.submit(ctx, "now the tests", first.session_id)
+second = await reviewer.submit(
+    ctx, "Which issue should I fix first?", first.session_id
+)
 second_result = await second
 ```
 {{< /tab >}}
 
 {{< tab "Java" >}}
 ```java
-SubagentSetup reviewer = (SubagentSetup) ctx.getResource("reviewer", 
ResourceType.AGENT);
+SubagentSetup reviewer =
+        (SubagentSetup) ctx.getResource("session_reviewer", 
ResourceType.AGENT);
 
-SubagentFuture first = reviewer.submit(ctx, "review the diff");
+SubagentFuture first = reviewer.submit(ctx, "Review this diff: ...");
 SubagentResult firstResult = first.await();
 
-SubagentFuture second = reviewer.submit(ctx, "now the tests", 
first.getSessionId());
+SubagentFuture second =
+        reviewer.submit(ctx, "Which issue should I fix first?", 
first.getSessionId());
 SubagentResult secondResult = second.await();
 ```
 {{< /tab >}}
 
 {{< /tabs >}}
 
+Internal sub-agents do not retain conversation context between calls, even when
+reusing a session ID.
+
 ### Cancel and resolve
 
 Resolve every handle you submit. An action that finishes with a handle it 
neither
@@ -1144,26 +1148,52 @@ three fields:
 | Field | Java | Carries |
 |---|---|---|
 | `success` | `isSuccess()` | Whether the call succeeded |
-| `result` | `getResult()` | The answer on success. For an internal sub-agent, 
the list of `OutputEvent` payloads in emission order. For an external one, 
whatever the setup produces, such as a string or an object |
+| `result` | `getResult()` | The answer on success |
 | `error_message` | `getErrorMessage()` | The reason on failure |
 
-A result a model observes must be JSON-compatible. That means string map keys,
-finite numbers, and no cycles. The framework normalizes the result on its way 
into
-the tool observation, and one it cannot normalize fails the delegation.
+**Internal sub-agent results**
 
-A failover restores the recorded result, and the two languages restore it
-differently. Python persists the payload with cloudpickle, so a replay hands 
it back
-with the type the sub-agent produced. Java persists it as JSON, so 
`getResult()`
-hands back a map after a replay, because recovery re-binds the recorded payload
-without your type. `getResult(Class<T>)` converts on the first run and after a
-replay alike, so prefer it when you want your own type back.
+An internal sub-agent returns a list of its `OutputEvent` payloads in emission
+order, even when it emits only one output. The following example reads the 
first
+output from the `reviewer` shown earlier:
 
-{{< tabs "Read a SubagentResult" >}}
+{{< tabs "Read an Internal Sub-agent Result" >}}
+
+{{< tab "Python" >}}
+```python
+if result.success:
+    ctx.send_event(OutputEvent(output=result.result[0]))
+else:
+    ctx.send_event(OutputEvent(output=f"failed: {result.error_message}"))
+```
+{{< /tab >}}
+
+{{< tab "Java" >}}
+```java
+import java.util.List;
+
+if (result.isSuccess()) {
+    List<?> outputs = (List<?>) result.getResult();
+    ctx.sendEvent(new OutputEvent(outputs.get(0)));
+} else {
+    ctx.sendEvent(new OutputEvent("failed: " + result.getErrorMessage()));
+}
+```
+{{< /tab >}}
+
+{{< /tabs >}}
+
+**External sub-agent results**
+
+An external sub-agent returns the value its setup supplies, such as a string 
or an
+object. This example assumes the setup returns a Python dictionary with a 
`verdict`
+key, or a Java `Review` bean with a public `verdict` field:
+
+{{< tabs "Read an External Sub-agent Result" >}}
 
 {{< tab "Python" >}}
 ```python
 if result.success:
-    # The payload keeps the type the sub-agent produced, after a replay too.
     ctx.send_event(OutputEvent(output=result.result["verdict"]))
 else:
     ctx.send_event(OutputEvent(output=f"failed: {result.error_message}"))
@@ -1173,7 +1203,6 @@ else:
 {{< tab "Java" >}}
 ```java
 if (result.isSuccess()) {
-    // Review is your own type, the shape this sub-agent produces.
     Review review = result.getResult(Review.class);
     ctx.sendEvent(new OutputEvent(review.verdict));
 } else {
@@ -1184,6 +1213,24 @@ if (result.isSuccess()) {
 
 {{< /tabs >}}
 
+In Java, use `getResult(Class<T>)` when reading a result as your own type. It 
also
+works after recovery; see [Result types after 
recovery](#result-types-after-recovery).
+
+**Results returned to a model**
+
+For a model-driven call, an internal sub-agent's tool observation is the JSON
+encoding of its full output list, even when the child emits a single output.
+
+An external setup can declare `result_type()` in Python or `getResultType()` 
in Java
+to shape the tool observation, for example to narrow a wider payload or 
convert it
+to a JSON-compatible form. By default, it uses the result as returned by the 
setup.
+This declaration affects only what the model observes; an action awaiting the 
call
+receives the result the setup produced.
+
+A result a model observes must be JSON-compatible. That means string map keys,
+finite numbers, and no cycles. The framework normalizes the result on its way 
into
+the tool observation, and one it cannot normalize fails the delegation.
+
 ### How failures surface
 
 An expected failure is a value, and an unexpected one is an exception. Return
@@ -1196,44 +1243,55 @@ triggers recovery.
 - A failed result reaches a model as a tool observation that names the 
sub-agent and
   the reason, so the model can decide what to do next. An exception while 
submitting
   or awaiting is reported the same way.
-- A failing child action does not fail the job. It surfaces through the 
caller's
-  result. The text in `error_message` is not a contract, so test for the 
failure
-  rather than for the message.
-- A result that is not JSON-compatible fails the delegation rather than the 
job.
+- Ordinary exceptions from a child action are returned as a failed result.
+  Cancellation and fatal errors still propagate. The text in `error_message` is
+  not a contract, so test for the failure rather than for the message.
+- For model-driven calls, a result that cannot be converted to JSON is reported
+  as a failed delegation.
 
 ## Durability and failover
 
-A sub-agent call always runs through
+Sub-agent calls use
 [durable execution]({{< ref 
"docs/development/create-agent/actions#durable-execution" >}}).
-Its result is persisted and replayed on recovery, so the work is not repeated 
after
-a failover. There is no durability toggle and no sub-agent timeout option.
-
-- Identities are replay-stable. Without explicit ids, the framework assigns
-  `session_id` and `call_id` from the executing action and the sub-agent's 
resource
-  name, so a replay of the same call gets the same ids. The ids are opaque, so 
do not
-  parse them. Two sub-agents called from one action never collide, because the
-  resource name is part of the namespace.
-- A deferred call runs under the durable id you build in `prepare`, so a replay
-  re-issues the same logical invocation. A remote keyed on the same values 
then reuses
-  the original work instead of starting a second one. The reconciler is 
optional.
-  Without it, a crash between the call landing and its result being persisted 
re-runs
-  the sub-agent and duplicates its side effects, so supply one whenever the 
work
-  cannot be repeated.
-- An async call reuses one identity for both its POST and its poll, so your 
service
-  sees at most one run per call.
-- An internal sub-agent's child actions have their own persisted state, so a 
replay
-  reuses the recorded child output instead of running the child again.
+Recovery reuses persisted results. Work whose result has not been persisted may
+run again, so external services need to handle retried requests.
+
+The framework generates stable `session_id` and `call_id` values for recovery.
+If you supply your own IDs, keep them unchanged when the same call is retried.
+
+- **Internal**: recovery reuses the child's persisted action state and outputs.
+  Work not yet recorded may run again.
+- **External deferred**: build the key returned by `prepare` from `session_id` 
and
+  `call_id`. Adding a timestamp or another changing value prevents recovery 
from
+  finding the original result. If the service completed the work but the result
+  was not persisted, a reconciler can look up the outcome. Without one, 
recovery
+  repeats the request. Use a reconciler or service-side deduplication when 
repeating
+  the work would cause duplicate effects. See [Optional 
reconciler](#optional-reconciler).
+- **External async**: if submission completed but was not recorded, recovery 
queries
+  the task's status and resubmits only when it reports `NOT_STARTED`. The 
service
+  must reliably identify the existing task using the same IDs. If a submitted 
task
+  can be missing from status queries, the service must also deduplicate 
repeated
+  submissions to avoid starting it twice. Status queries, result fetches, and
+  cancellation requests must be safe to repeat. See
+  [Recovery and cancellation](#recovery-and-cancellation).
+
+### Result types after recovery
+
+Python persists result payloads with cloudpickle and restores their Python 
types.
+Java persists them as JSON, so a custom object returned by `getResult()` can be
+restored as a map after recovery. Use `getResult(Class<T>)` to obtain the 
expected
+Java type on both the first execution and after recovery.
 
 ## Metrics and Trace Log
 
-Only the delegations a model requests are reported. A sub-agent you drive 
yourself
-from an action emits no sub-agent metrics and no sub-agent trace records. Add
-[custom metrics]({{< ref 
"docs/operations/monitoring#how-to-add-custom-metrics" >}})
-if you need to observe that path.
+The framework reports sub-agent metrics for model-driven calls, grouped by the
+registered sub-agent name under the `subagent` scope. These calls are not also
+counted as Tool calls.
 
-A model-driven delegation is counted under a `subagent` scope keyed by the 
registered
-sub-agent name. It is reported there only and never also as a tool, so the 
scope's
-cardinality is bounded by your plan.
+Calls made directly from actions do not automatically produce sub-agent 
metrics or
+sub-agent Trace Log records. Add
+[custom metrics]({{< ref 
"docs/operations/monitoring#how-to-add-custom-metrics" >}})
+if you need to monitor those calls.
 
 | Metric | Counts |
 |---|---|
@@ -1244,33 +1302,32 @@ cardinality is bounded by your plan.
 Full metric paths and types are in
 [Execution Metrics]({{< ref "docs/operations/monitoring#execution-metrics" 
>}}).
 
-The same delegations appear in
-[Trace Log]({{< ref "docs/operations/monitoring#trace-log" >}}) as call 
records with
-`entityType: "subagent"`. The default target records events only, so select the
-`subagent` scope, or `ALL`, to get them.
+To include model-driven calls in
+[Trace Log]({{< ref "docs/operations/monitoring#trace-log" >}}), configure
+[`trace-log.targets`]({{< ref 
"docs/operations/configuration#trace-log-targets" >}})
+with `scope: {subagent: "*"}` or `scope: ALL`. These records have
+`entityType: "subagent"`. The default configuration records only Events;
+Trace Log selection does not affect metrics collection.
 
 ## Restrictions
 
-- Recursive registration is rejected when the plan is built, whether the cycle 
runs
-  through a child, through the root agent, or directly back to itself.
-- Registering the same child `Agent` instance under two names compiles it once 
and
-  shares the plan.
-- `_subagent_` is a reserved prefix. A sub-agent reaches a model as a tool 
whose name
-  carries it, and no tool may be registered under the prefix, including a tool 
an MCP
-  server advertises. A tool and a sub-agent may therefore share a name, 
because the
-  prefix keeps the two namespaces apart. See
+- Internal sub-agent registrations must not form a cycle. An agent cannot 
register
+  itself as a child, directly or through other agents.
+- Tool names must not start with the reserved `_subagent_` prefix. This also 
applies
+  to Tools provided by MCP servers. A Tool and a sub-agent can otherwise use 
the
+  same registered name. See
   [Tool Use]({{< ref "docs/development/calling-llms/tool_use" >}}).
-- A sub-agent declared in one language cannot be driven from the other. 
Delegation
-  resolves the `AGENT` resource in the executing language only.
-- Resolving an internal sub-agent call in Java requires JDK 21+ with the 
Continuation
-  API. On an older runtime the call fails fast rather than hanging. Python 
callers
-  and external sub-agents have no JDK requirement.
-- When a model emits several sub-agent calls in one turn, and 
`tool-call.async` is
-  enabled with `async.batch.parallelism` above 1, the framework submits every 
call
-  before awaiting any of them. An async sub-agent starts its remote run during 
that
-  submit, so those runs overlap. A deferred or internal call does its work 
while it
-  is awaited, so it does not overlap on this path. Batching handles yourself 
differs
-  for an internal child, as
-  [Fan out to several sub-agents](#fan-out-to-several-sub-agents) describes. 
Results
-  still land on their own call ids, in the model's call order. See
-  [Configuration]({{< ref "docs/operations/configuration" >}}).
+- Create an internal child with the same Java or Python Agent API as its 
parent.
+  An external setup and the action invoking it must use the same language;
+  cross-language calls to external setups are not supported.
+- Calling an internal sub-agent from Java requires JDK 21+ with the 
Continuation
+  API available. This additional requirement does not apply to Python callers 
or
+  external sub-agents.
+- There is no per-call sub-agent timeout setting.
+- For multiple model-driven calls, enabling `tool-call.async` with
+  `async.batch.parallelism` above 1 allows external async tasks to overlap if 
the
+  remote service supports concurrent execution. Internal and external deferred
+  calls still run one at a time on this path. Results are returned in the 
model's
+  call order. For concurrent internal calls from an action, see
+  [Fan out to several sub-agents](#fan-out-to-several-sub-agents). See
+  [Configuration]({{< ref "docs/operations/configuration" >}}) for these 
settings.

Reply via email to