yunfengzhou-hub commented on code in PR #1268:
URL: https://github.com/apache/flink-agents/pull/1268#discussion_r4300862261
##########
docs/content/docs/development/multi-agent/subagents.md:
##########
@@ -24,30 +24,170 @@ specific language governing permissions and limitations
under the License.
-->
-A sub-agent handles a task delegated by a parent agent. An internal sub-agent
runs
-an agent workflow in the same job. An external sub-agent calls an agent service
-through a `SubagentSetup` implementation.
+## Overview
-## Declare an internal sub-agent
+A sub-agent is an agent that another agent delegates a task to. The parent
hands it
+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.
-Use `InternalSubagentResourceDescriptorBuilder` to attach a description and
input
-schema to a child agent. The builder returns a normal resource descriptor for
`addResource`
-or `add_resource`.
+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.
+
+Every sub-agent call runs through
+[durable execution]({{< ref
"docs/development/create-agent/actions#durable-execution" >}}).
+There is no durability toggle and no sub-agent timeout option.
+
+## 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, and a YAML file can declare it.
+
+### Internal: compile a child agent
+
+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.
+
+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.
+
+#### A workflow agent as the child
+
+The example child owns a chat model and answers each review request with its
+verdict. Written this way it is an ordinary agent. The registration that
follows is
+what makes it a sub-agent. Its action receives the submitted input as an
+`InputEvent` and sends its answer as `OutputEvent`s.
+
+{{< tabs "Write a Child Agent" >}}
+
+{{< tab "Python" >}}
+```python
+class ReviewAgent(Agent):
+ @chat_model_connection
+ @staticmethod
+ def review_connection() -> ResourceDescriptor:
+ return ResourceDescriptor(
+ clazz=ResourceName.ChatModel.OLLAMA_CONNECTION,
+ base_url="http://localhost:11434",
+ )
+
+ @chat_model_setup
+ @staticmethod
+ def review_model() -> ResourceDescriptor:
+ return ResourceDescriptor(
+ clazz=ResourceName.ChatModel.OLLAMA_SETUP,
+ connection="review_connection",
+ model="qwen3:8b",
+ )
+
+ @action(EventType.InputEvent)
+ @staticmethod
+ def ask(event: Event, ctx: RunnerContext) -> None:
+ task = InputEvent.from_event(event).input
+ # A model-driven call passes the arguments map; a caller-driven call
+ # passes the submitted object.
+ diff = task["code"] if isinstance(task, dict) else str(task)
+ ctx.send_event(
+ ChatRequestEvent(
+ model="review_model",
+ messages=[ChatMessage.user(f"Review this diff:\n{diff}")],
+ )
+ )
+
+ @action(EventType.ChatResponseEvent)
+ @staticmethod
+ def answer(event: Event, ctx: RunnerContext) -> None:
+ response = ChatResponseEvent.from_event(event).response
+ ctx.send_event(OutputEvent(output=response.text))
+```
+{{< /tab >}}
+
+{{< tab "Java" >}}
```java
-import org.apache.flink.agents.api.resource.ResourceType;
-import
org.apache.flink.agents.api.subagent.InternalSubagentResourceDescriptorBuilder;
+public class ReviewAgent extends Agent {
-parent.addResource(
- "reviewer",
- ResourceType.AGENT,
- new InternalSubagentResourceDescriptorBuilder()
- .agent(new ReviewAgent())
- .description("Review code for correctness and potential defects")
- .inputType(ReviewRequest.class)
- .build());
+ @ChatModelConnection
+ public static ResourceDescriptor reviewConnection() {
+ return
ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.OLLAMA_CONNECTION)
+ .addInitialArgument("endpoint", "http://localhost:11434")
+ .build();
+ }
+
+ @ChatModelSetup
+ public static ResourceDescriptor reviewModel() {
+ return
ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.OLLAMA_SETUP)
+ .addInitialArgument("connection", "reviewConnection")
+ .addInitialArgument("model", "qwen3:8b")
+ .build();
+ }
+
+ @Action(EventType.InputEvent)
+ public static void ask(Event event, RunnerContext ctx) throws Exception {
+ Object task = InputEvent.fromEvent(event).getInput();
+ // A model-driven call passes the arguments map; a caller-driven call
+ // passes the submitted object.
+ String diff =
+ task instanceof Map
+ ? String.valueOf(((Map<?, ?>) task).get("code"))
+ : String.valueOf(task);
+ ctx.sendEvent(
+ new ChatRequestEvent(
+ "reviewModel",
+ List.of(new ChatMessage(MessageRole.USER, "Review this
diff:\n" + diff))));
+ }
+
+ @Action(EventType.ChatResponseEvent)
+ public static void answer(Event event, RunnerContext ctx) throws Exception
{
+ ctx.sendEvent(new
OutputEvent(ChatResponseEvent.fromEvent(event).getResponse().getText()));
+ }
+}
```
+{{< /tab >}}
+
+{{< /tabs >}}
+
+While a call runs, the child behaves like a scoped agent of its own:
+
+- 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.
+- It resolves resources against its own plan. Register the resources a child
uses,
+ such as its chat model connection, on the child itself. In Python a resource
+ registered solely on the parent is not visible to the child.
+- It runs against memory isolated from the caller's, so it never reads what the
+ caller persisted. The child's own actions share that memory within one call.
+ Isolation is per call. Continuing a session does not carry memory across
calls.
Review Comment:
Fixed with your wording.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]