Eino ADK: Agent Collaboration

Collaboration Overview

ADK defines collaboration and composition primitives to build multi‑agent systems. This page introduces collaboration modes, input context strategies, decision autonomy, and composition types with code and diagrams.

Collaboration Primitives

  • Collaboration mode

    ModeDescription
    TransferHand off the task to another agent. The current agent exits and does not wait for the child agent’s completion.
    ToolCall (AgentAsTool)Treat an agent as a tool, wait for its response, and use the output for subsequent processing.
  • Input context strategy

    StrategyDescription
    Upstream full dialogueReceive complete conversation history from upstream agents.
    New task descriptionIgnore upstream dialogue and generate a fresh summary as input for the child agent.
  • Decision autonomy

    AutonomyDescription
    AutonomousAgent internally selects downstream agents (often via LLM) when needed. Even preset logic is considered autonomous from the outside.
    PresetAgent execution order is predetermined and predictable.

Composition Types

TypeDescriptionDiagramCollabContextDecision
SubAgentsBuild a parent agent with a list of named subagents, forming a tree. Agent names must be unique within the tree.TransferUpstream full dialogueAutonomous
SequentialRun subagents in order once, then finish.TransferUpstream full dialoguePreset
ParallelRun subagents concurrently over shared input; finish after all complete.TransferUpstream full dialoguePreset
LoopRun subagents in sequence repeatedly until termination.TransferUpstream full dialoguePreset
AgentAsToolConvert an agent into a tool callable by others. `ChatModelAgent` supports this directly.ToolCallNew task descriptionAutonomous

Context Passing

ADK provides two core mechanisms: History and SessionValues.

History

Concept

  • History corresponds to the “Upstream full dialogue” strategy. Every AgentEvent produced by upstream agents is saved into History.
  • When invoking a new Agent (Workflow/Transfer), History events are converted and appended to that Agent’s AgentInput.

By default, assistant/tool messages from other agents are converted into user messages. This tells the current LLM: “Agent_A called some_tool with some_result. Now it’s your turn to decide.” It treats other agents’ behavior as external information rather than the current agent’s own actions, avoiding role confusion.

Filtering by RunPath

  • When building AgentInput for an Agent, only include History events whose RunPath belong to the current Agent’s RunPath (equal or prefix).

Definition: RunPathA “belongs to” RunPathB when RunPathA equals RunPathB or RunPathA is a prefix of RunPathB.

Examples of RunPath in different orchestrations:

ExampleRunPath
  • Agent: [Agent]
  • SubAgent: [Agent, SubAgent]
  • Agent: [Agent]
  • Agent (after function call): [Agent]
  • Agent1: [SequentialAgent, LoopAgent, Agent1]
  • Agent2: [SequentialAgent, LoopAgent, Agent1, Agent2]
  • Agent1: [SequentialAgent, LoopAgent, Agent1, Agent2, Agent1]
  • Agent2: [SequentialAgent, LoopAgent, Agent1, Agent2, Agent1, Agent2]
  • Agent3: [SequentialAgent, LoopAgent, Agent3]
  • Agent4: [SequentialAgent, LoopAgent, Agent3, ParallelAgent, Agent4]
  • Agent5: [SequentialAgent, LoopAgent, Agent3, ParallelAgent, Agent5]
  • Agent6: [SequentialAgent, LoopAgent, Agent3, ParallelAgent, Agent6]
  • Agent: [Agent]
  • SubAgent: [Agent, SubAgent]
  • Agent: [Agent, SubAgent, Agent]
  • Customize via WithHistoryRewriter:

    type HistoryRewriter func(ctx context.Context, entries []*HistoryEntry) ([]Message, error)
    func WithHistoryRewriter(h HistoryRewriter) AgentOption
    

    SessionValues

    • Global KV store per run; thread‑safe helpers:
    func GetSessionValues(ctx context.Context) map[string]any
    func AddSessionValues(ctx context.Context, kvs map[string]any)
    func GetSessionValue(ctx context.Context, key string) (any, bool)
    func AddSessionValue(ctx context.Context, key string, value any)
    

    Note: SessionValues are implemented via Context. Runner re‑initializes Context when starting a run, so calling AddSessionValues or AddSessionValue outside of Runner.Run will not take effect.

    Inject initial values on run:

    runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: agent})
    iterator := runner.Run(ctx, []adk.Message{schema.UserMessage("xxx")},
        adk.WithSessionValues(map[string]any{
            PlanSessionKey:      123,
            UserInputSessionKey: []adk.Message{schema.UserMessage("yyy")},
        }),
    )
    

    Transfer SubAgents

    • Emit a Transfer action to hand off control:
    event := adk.NewTransferToAgentAction("dest agent name")
    
    • Register subagents before running:
    func SetSubAgents(ctx context.Context, agent Agent, subAgents []Agent) (Agent, error)
    
    • Notes:
      • Transfer hands off control; parent does not summarize after child ends
      • Child receives original input; parent output is context only

    – Agents can implement OnSubAgents to handle registration callbacks.

    // github.com/cloudwego/eino/adk/interface.go
    type OnSubAgents interface {
        OnSetSubAgents(ctx context.Context, subAgents []Agent) error
        OnSetAsSubAgent(ctx context.Context, parent Agent) error
        OnDisallowTransferToParent(ctx context.Context) error
    }
    

    Example: Weather + Chat via Transfer

    Three ChatModelAgents: a router, a weather agent (tool‑enabled), and a general chat agent. The router uses Transfer to hand off requests based on intent.

    import (
        "context"
        "fmt"
        "log"
        "os"
    
        "github.com/cloudwego/eino-ext/components/model/openai"
        "github.com/cloudwego/eino/adk"
        "github.com/cloudwego/eino/components/model"
        "github.com/cloudwego/eino/components/tool"
        "github.com/cloudwego/eino/components/tool/utils"
        "github.com/cloudwego/eino/compose"
    )
    
    func newChatModel() model.ToolCallingChatModel {
        cm, err := openai.NewChatModel(context.Background(), &openai.ChatModelConfig{
           APIKey: os.Getenv("OPENAI_API_KEY"),
           Model:  os.Getenv("OPENAI_MODEL"),
        })
        if err != nil {
           log.Fatal(err)
        }
        return cm
    }
    
    type GetWeatherInput struct {
        City string `json:"city"`
    }
    
    func NewWeatherAgent() adk.Agent {
        weatherTool, err := utils.InferTool(
           "get_weather",
           "Gets the current weather for a specific city.",
           func(ctx context.Context, input *GetWeatherInput) (string, error) {
              return fmt.Sprintf(`the temperature in %s is 25°C`, input.City), nil
           },
        )
        if err != nil {
           log.Fatal(err)
        }
    
        a, err := adk.NewChatModelAgent(context.Background(), &adk.ChatModelAgentConfig{
           Name:        "WeatherAgent",
           Description: "This agent can get the current weather for a given city.",
           Instruction: "Your sole purpose is to get the current weather for a given city by using the 'get_weather' tool. After calling the tool, report the result directly to the user.",
           Model:       newChatModel(),
           ToolsConfig: adk.ToolsConfig{
              ToolsNodeConfig: compose.ToolsNodeConfig{
                 Tools: []tool.BaseTool{weatherTool},
              },
           },
        })
        if err != nil {
           log.Fatal(err)
        }
        return a
    }
    
    func NewChatAgent() adk.Agent {
        a, err := adk.NewChatModelAgent(context.Background(), &adk.ChatModelAgentConfig{
           Name:        "ChatAgent",
           Description: "A general-purpose agent for handling conversational chat.",
           Instruction: "You are a friendly conversational assistant. Your role is to handle general chit-chat and answer questions that are not related to any specific tool-based tasks.",
           Model:       newChatModel(),
        })
        if err != nil {
           log.Fatal(err)
        }
        return a
    }
    
    func NewRouterAgent() adk.Agent {
        a, err := adk.NewChatModelAgent(context.Background(), &adk.ChatModelAgentConfig{
           Name:        "RouterAgent",
           Description: "A manual router that transfers tasks to other expert agents.",
           Instruction: `You are an intelligent task router. Your responsibility is to analyze the user's request and delegate it to the most appropriate expert agent.If no Agent can handle the task, simply inform the user it cannot be processed.`,
           Model:       newChatModel(),
        })
        if err != nil {
           log.Fatal(err)
        }
        return a
    }
    

    Wire them together and run:

    import (
        "context"
        "fmt"
        "log"
        "os"
    
        "github.com/cloudwego/eino/adk"
    )
    
    func main() {
        weatherAgent := NewWeatherAgent()
        chatAgent := NewChatAgent()
        routerAgent := NewRouterAgent()
    
        ctx := context.Background()
        a, err := adk.SetSubAgents(ctx, routerAgent, []adk.Agent{chatAgent, weatherAgent})
        if err != nil {
           log.Fatal(err)
        }
    
        runner := adk.NewRunner(ctx, adk.RunnerConfig{
           Agent: a,
        })
    
        // query weather
        println("\n\n>>>>>>>>>query weather<<<<<<<<<")
        iter := runner.Query(ctx, "What's the weather in Beijing?")
        for {
           event, ok := iter.Next()
           if !ok {
              break
           }
           if event.Err != nil {
              log.Fatal(event.Err)
           }
           if event.Action != nil {
              fmt.Printf("\nAgent[%s]: transfer to %+v\n\n======\n", event.AgentName, event.Action.TransferToAgent.DestAgentName)
           } else {
              fmt.Printf("\nAgent[%s]:\n%+v\n\n======\n", event.AgentName, event.Output.MessageOutput.Message)
           }
        }
    
        // failed to route
        println("\n\n>>>>>>>>>failed to route<<<<<<<<<")
        iter = runner.Query(ctx, "Book me a flight from New York to London tomorrow.")
        for {
           event, ok := iter.Next()
           if !ok {
              break
           }
           if event.Err != nil {
              log.Fatal(event.Err)
           }
           if event.Action != nil {
              fmt.Printf("\nAgent[%s]: transfer to %+v\n\n======\n", event.AgentName, event.Action.TransferToAgent.DestAgentName)
           } else {
              fmt.Printf("\nAgent[%s]:\n%+v\n\n======\n", event.AgentName, event.Output.MessageOutput.Message)
           }
        }
    }
    

    Sample output:

    >>>>>>>>>query weather<<<<<<<<<
    Agent[RouterAgent]:
    assistant: 
    tool_calls:
    {Index:<nil> ID:call_SKNsPwKCTdp1oHxSlAFt8sO6 Type:function Function:{Name:transfer_to_agent Arguments:{"agent_name":"WeatherAgent"}} Extra:map[]}
    
    finish_reason: tool_calls
    usage: &{201 17 218}
    ======
    Agent[RouterAgent]: transfer to WeatherAgent
    ======
    Agent[WeatherAgent]:
    assistant: 
    tool_calls:
    {Index:<nil> ID:call_QMBdUwKj84hKDAwMMX1gOiES Type:function Function:{Name:get_weather Arguments:{"city":"Beijing"}} Extra:map[]}
    
    finish_reason: tool_calls
    usage: &{255 15 270}
    ======
    Agent[WeatherAgent]:
    tool: the temperature in Beijing is 25°C
    tool_call_id: call_QMBdUwKj84hKDAwMMX1gOiES
    tool_call_name: get_weather
    ======
    Agent[WeatherAgent]:
    assistant: The current temperature in Beijing is 25°C.
    finish_reason: stop
    usage: &{286 11 297}
    ======
    
    >>>>>>>>>failed to route<<<<<<<<<
    Agent[RouterAgent]:
    assistant: I'm unable to assist with booking flights. Please use a relevant travel service or booking platform to make your reservation.
    finish_reason: stop
    usage: &{206 23 229}
    ======
    

    Other OnSubAgents callbacks when registering child agents:

    • OnSetAsSubAgent: register parent info to the agent
    • OnDisallowTransferToParent: when WithDisallowTransferToParent is set, inform the agent not to transfer back to its parent
    adk.SetSubAgents(
        ctx,
        Agent1,
        []adk.Agent{
           adk.AgentWithOptions(ctx, Agent2, adk.WithDisallowTransferToParent()),
        },
    )
    

    Deterministic Transfer

    Wrap a subagent so it automatically transfers back to a target (e.g., supervisor) after finishing:

    type DeterministicTransferConfig struct {
        Agent        Agent
        ToAgentNames []string
    }
    
    func AgentWithDeterministicTransferTo(_ context.Context, config *DeterministicTransferConfig) Agent
    

    Used by Supervisor to ensure subagents return control deterministically:

    // github.com/cloudwego/eino/adk/prebuilt/supervisor.go
    
    type SupervisorConfig struct {
            Supervisor adk.Agent
            SubAgents  []adk.Agent
    }
    
    func NewSupervisor(ctx context.Context, conf *SupervisorConfig) (adk.Agent, error) {
            subAgents := make([]adk.Agent, 0, len(conf.SubAgents))
            supervisorName := conf.Supervisor.Name(ctx)
            for _, subAgent := range conf.SubAgents {
                    subAgents = append(subAgents, adk.AgentWithDeterministicTransferTo(ctx, &adk.DeterministicTransferConfig{
                            Agent:        subAgent,
                            ToAgentNames: []string{supervisorName},
                    }))
            }
    
            return adk.SetSubAgents(ctx, conf.Supervisor, subAgents)
    }
    

    Workflow Agents

    WorkflowAgent runs agents according to a preset flow. ADK provides three base Workflow agents: Sequential, Parallel, and Loop. They can be nested to build more complex tasks.

    By default, each agent’s input in a Workflow is generated via the History mechanism described earlier; you can customize AgentInput generation with WithHistoryRewriter.

    When an agent emits an ExitAction event, the Workflow agent exits immediately, regardless of remaining agents.

    See details and examples: /docs/eino/core_modules/eino_adk/agent_implementation/workflow

    SequentialAgent

    Run a series of agents in the provided order:

    type SequentialAgentConfig struct {
        Name        string
        Description string
        SubAgents   []Agent
    }
    
    func NewSequentialAgent(ctx context.Context, config *SequentialAgentConfig) (Agent, error)
    

    LoopAgent

    Based on SequentialAgent; after completing one run, it starts from the beginning again:

    type LoopAgentConfig struct {
        Name        string
        Description string
        SubAgents   []Agent
    
        MaxIterations int
    }
    
    func NewLoopAgent(ctx context.Context, config *LoopAgentConfig) (Agent, error)
    

    ParallelAgent

    Run agents concurrently:

    type ParallelAgentConfig struct {
        Name        string
        Description string
        SubAgents   []Agent
    }
    
    func NewParallelAgent(ctx context.Context, config *ParallelAgentConfig) (Agent, error)
    

    Last modified December 11, 2025 : feat(eino): sync zh documents (#1474) (9585944)