Files
2026-07-13 12:49:10 +08:00

520 lines
18 KiB
Go

/*
* Copyright 2025 CloudWeGo Authors
*
* Licensed 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 adk
import (
"bytes"
"context"
"encoding/gob"
"fmt"
"io"
"github.com/cloudwego/eino/components"
"github.com/cloudwego/eino/internal/core"
"github.com/cloudwego/eino/schema"
)
// ComponentOfAgent is the component type identifier for ADK agents in callbacks.
// Use this to filter callback events to only agent-related events.
const ComponentOfAgent components.Component = "Agent"
// ComponentOfAgenticAgent is the component type identifier for ADK agents
// that use *schema.AgenticMessage in callbacks.
const ComponentOfAgenticAgent components.Component = "AgenticAgent"
// MessageType is the sealed type constraint for message types used in ADK.
// Only *schema.Message and *schema.AgenticMessage satisfy this constraint.
// External packages cannot add new types to this union; all generic functions
// in ADK use exhaustive type switches on these two types.
type MessageType interface {
*schema.Message | *schema.AgenticMessage
}
type Message = *schema.Message
type MessageStream = *schema.StreamReader[Message]
type AgenticMessage = *schema.AgenticMessage
type AgenticMessageStream = *schema.StreamReader[AgenticMessage]
// isNilMessage checks whether a generic message value is nil.
// Direct `msg == nil` does not compile for generic pointer types in Go;
// the canonical workaround is to compare through the `any` interface.
func isNilMessage[M MessageType](msg M) bool {
var zero M
return any(msg) == any(zero)
}
// TypedMessageVariant represents a message output from an agent event.
// It carries either a complete message or a streaming reader, along with
// metadata describing the event's origin.
//
// Role and ToolName are only meaningful for *schema.Message events. For
// *schema.AgenticMessage events (created via EventFromAgenticMessage), these
// fields are always zero-valued because AgenticMessage carries tool results as
// ContentBlocks within the message itself and does not support agent transfer.
//
// For *schema.Message events, Role and ToolName exist independently of the inner
// Message because in streaming mode (IsStreaming=true, Message=nil), the message
// has not materialized yet and the consumer needs metadata without consuming the stream.
type TypedMessageVariant[M MessageType] struct {
IsStreaming bool
Message M
MessageStream *schema.StreamReader[M]
// Role indicates the origin of this event within the agent's ReAct loop.
// Only meaningful for *schema.Message events:
// - schema.Assistant: the event carries model output (generation or stream).
// - schema.Tool: the event carries a tool execution result.
// Always zero-valued for *schema.AgenticMessage events; use AgenticRole instead.
Role schema.RoleType
// AgenticRole indicates the role of the agentic message (assistant, user, system).
// Only meaningful for *schema.AgenticMessage events.
// In streaming mode, this is available before consuming the stream.
// Always zero-valued for *schema.Message events; use Role instead.
AgenticRole schema.AgenticRoleType
// ToolName is the name of the tool that produced this event.
// Only meaningful for *schema.Message events: non-empty when Role == schema.Tool.
// In streaming mode, this is the only way to identify the source tool before
// the stream is consumed.
// Always empty for *schema.AgenticMessage events.
ToolName string
}
func (mv *TypedMessageVariant[M]) GetMessage() (M, error) {
if mv.IsStreaming {
return concatMessageStream(mv.MessageStream)
}
return mv.Message, nil
}
type MessageVariant = TypedMessageVariant[*schema.Message]
type messageVariantSerialization struct {
IsStreaming bool
Message Message
MessageStream Message
Role schema.RoleType
ToolName string
}
type agenticMessageVariantSerialization struct {
IsStreaming bool
Message *schema.AgenticMessage
MessageStream *schema.AgenticMessage
Role schema.RoleType
AgenticRole schema.AgenticRoleType
ToolName string
}
func (mv *TypedMessageVariant[M]) GobEncode() ([]byte, error) {
if mvMsg, ok := any(mv).(*TypedMessageVariant[*schema.Message]); ok {
return gobEncodeMessageVariant(mvMsg)
}
if mvAgentic, ok := any(mv).(*TypedMessageVariant[*schema.AgenticMessage]); ok {
return gobEncodeAgenticMessageVariant(mvAgentic)
}
return nil, fmt.Errorf("gob encoding not supported for this message type")
}
func (mv *TypedMessageVariant[M]) GobDecode(b []byte) error {
if mvMsg, ok := any(mv).(*TypedMessageVariant[*schema.Message]); ok {
return gobDecodeMessageVariant(mvMsg, b)
}
if mvAgentic, ok := any(mv).(*TypedMessageVariant[*schema.AgenticMessage]); ok {
return gobDecodeAgenticMessageVariant(mvAgentic, b)
}
return fmt.Errorf("gob decoding not supported for this message type")
}
func gobEncodeMessageVariant(mv *TypedMessageVariant[*schema.Message]) ([]byte, error) {
s := &messageVariantSerialization{
IsStreaming: mv.IsStreaming,
Message: mv.Message,
Role: mv.Role,
ToolName: mv.ToolName,
}
if mv.IsStreaming {
var messages []Message
for {
frame, err := mv.MessageStream.Recv()
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("error receiving message stream: %w", err)
}
messages = append(messages, frame)
}
m, err := schema.ConcatMessages(messages)
if err != nil {
return nil, fmt.Errorf("failed to encode message: cannot concat message stream: %w", err)
}
s.MessageStream = m
}
buf := &bytes.Buffer{}
err := gob.NewEncoder(buf).Encode(s)
if err != nil {
return nil, fmt.Errorf("failed to gob encode message variant: %w", err)
}
return buf.Bytes(), nil
}
func gobDecodeMessageVariant(mv *TypedMessageVariant[*schema.Message], b []byte) error {
s := &messageVariantSerialization{}
err := gob.NewDecoder(bytes.NewReader(b)).Decode(s)
if err != nil {
return fmt.Errorf("failed to decoding message variant: %w", err)
}
mv.IsStreaming = s.IsStreaming
mv.Message = s.Message
mv.Role = s.Role
mv.ToolName = s.ToolName
if s.MessageStream != nil {
mv.MessageStream = schema.StreamReaderFromArray([]*schema.Message{s.MessageStream})
}
return nil
}
func gobEncodeAgenticMessageVariant(mv *TypedMessageVariant[*schema.AgenticMessage]) ([]byte, error) {
s := &agenticMessageVariantSerialization{
IsStreaming: mv.IsStreaming,
Message: mv.Message,
Role: mv.Role,
AgenticRole: mv.AgenticRole,
ToolName: mv.ToolName,
}
if mv.IsStreaming {
var messages []*schema.AgenticMessage
for {
frame, err := mv.MessageStream.Recv()
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("error receiving agentic message stream: %w", err)
}
messages = append(messages, frame)
}
m, err := schema.ConcatAgenticMessages(messages)
if err != nil {
return nil, fmt.Errorf("failed to encode agentic message: cannot concat message stream: %w", err)
}
s.MessageStream = m
}
buf := &bytes.Buffer{}
err := gob.NewEncoder(buf).Encode(s)
if err != nil {
return nil, fmt.Errorf("failed to gob encode agentic message variant: %w", err)
}
return buf.Bytes(), nil
}
func gobDecodeAgenticMessageVariant(mv *TypedMessageVariant[*schema.AgenticMessage], b []byte) error {
s := &agenticMessageVariantSerialization{}
err := gob.NewDecoder(bytes.NewReader(b)).Decode(s)
if err != nil {
return fmt.Errorf("failed to decode agentic message variant: %w", err)
}
mv.IsStreaming = s.IsStreaming
mv.Message = s.Message
mv.Role = s.Role
mv.AgenticRole = s.AgenticRole
mv.ToolName = s.ToolName
if s.MessageStream != nil {
mv.MessageStream = schema.StreamReaderFromArray([]*schema.AgenticMessage{s.MessageStream})
}
return nil
}
// typedEventFromMessage creates a TypedAgentEvent containing the given message and optional stream.
func typedEventFromMessage[M MessageType](msg M, msgStream *schema.StreamReader[M],
role schema.RoleType, toolName string) *TypedAgentEvent[M] {
return &TypedAgentEvent[M]{
Output: &TypedAgentOutput[M]{
MessageOutput: &TypedMessageVariant[M]{
IsStreaming: msgStream != nil,
Message: msg,
MessageStream: msgStream,
Role: role,
ToolName: toolName,
},
},
}
}
// typedModelOutputEvent creates a model-output event for the generic path.
// For *schema.Message, Role is set to schema.Assistant.
// For *schema.AgenticMessage, AgenticRole is set to schema.AgenticRoleTypeAssistant.
func typedModelOutputEvent[M MessageType](msg M, msgStream *schema.StreamReader[M]) *TypedAgentEvent[M] {
var role schema.RoleType
var agenticRole schema.AgenticRoleType
var zero M
if _, ok := any(zero).(*schema.Message); ok {
role = schema.Assistant
} else {
agenticRole = schema.AgenticRoleTypeAssistant
}
event := typedEventFromMessage(msg, msgStream, role, "")
event.Output.MessageOutput.AgenticRole = agenticRole
return event
}
// EventFromMessage creates an AgentEvent containing the given message and optional stream.
//
// role identifies the origin of this event:
// - schema.Assistant: model output (generation or stream).
// - schema.Tool: tool execution result; toolName must be non-empty.
//
// For *schema.AgenticMessage events, use EventFromAgenticMessage instead.
func EventFromMessage(msg Message, msgStream *schema.StreamReader[Message],
role schema.RoleType, toolName string) *AgentEvent {
return typedEventFromMessage(msg, msgStream, role, toolName)
}
// EventFromAgenticMessage creates a TypedAgentEvent for the AgenticMessage path.
// Unlike EventFromMessage, it does not require role or toolName parameters because
// AgenticMessage carries tool results as ContentBlocks within the message itself,
// and does not support agent transfer.
//
// agenticRole identifies the role of the message (e.g. schema.AgenticRoleTypeAssistant).
// In streaming mode, the role is available on the event before consuming the stream.
func EventFromAgenticMessage(msg AgenticMessage, msgStream AgenticMessageStream, agenticRole schema.AgenticRoleType) *TypedAgentEvent[AgenticMessage] {
return &TypedAgentEvent[AgenticMessage]{
Output: &TypedAgentOutput[AgenticMessage]{
MessageOutput: &TypedMessageVariant[AgenticMessage]{
IsStreaming: msgStream != nil,
Message: msg,
MessageStream: msgStream,
AgenticRole: agenticRole,
},
},
}
}
// TransferToAgentAction represents a transfer-to-agent action.
//
// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven
// to be more effective empirically. Consider using ChatModelAgent with AgentTool
// or DeepAgent instead for most multi-agent scenarios.
type TransferToAgentAction struct {
DestAgentName string
}
type TypedAgentOutput[M MessageType] struct {
MessageOutput *TypedMessageVariant[M]
CustomizedOutput any
}
type AgentOutput = TypedAgentOutput[*schema.Message]
// NewTransferToAgentAction creates an action to transfer to the specified agent.
//
// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven
// to be more effective empirically. Consider using ChatModelAgent with AgentTool
// or DeepAgent instead for most multi-agent scenarios.
func NewTransferToAgentAction(destAgentName string) *AgentAction {
return &AgentAction{TransferToAgent: &TransferToAgentAction{DestAgentName: destAgentName}}
}
// NewExitAction creates an action that signals the agent to exit.
//
// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven
// to be more effective empirically. Consider using ChatModelAgent with AgentTool
// or DeepAgent instead for most multi-agent scenarios.
func NewExitAction() *AgentAction {
return &AgentAction{Exit: true}
}
// AgentAction represents actions that an agent can emit during execution.
//
// Action Scoping in Agent Tools:
// When an agent is wrapped as an agent tool (via NewAgentTool), actions emitted by the inner agent
// are scoped to the tool boundary:
// - Interrupted: Propagated via CompositeInterrupt to allow proper interrupt/resume across boundaries
// - Exit, TransferToAgent, BreakLoop: Ignored outside the agent tool; these actions only affect
// the inner agent's execution and do not propagate to the parent agent
//
// This scoping ensures that nested agents cannot unexpectedly terminate or transfer control
// of their parent agent's execution flow.
type AgentAction struct {
Exit bool
Interrupted *InterruptInfo
TransferToAgent *TransferToAgentAction
BreakLoop *BreakLoopAction
CustomizedAction any
internalInterrupted *core.InterruptSignal
}
// RunStep represents a step in the agent execution path.
// CheckpointSchema: persisted via serialization.RunCtx (gob).
//
// NOT RECOMMENDED: RunStep is mainly relevant for agent transfer and workflow agents,
// which have not proven to be more effective empirically. Consider using ChatModelAgent
// with AgentTool or DeepAgent instead for most multi-agent scenarios.
type RunStep struct {
agentName string
}
func init() {
schema.RegisterName[[]RunStep]("eino_run_step_list")
}
func (r *RunStep) String() string {
return r.agentName
}
func (r *RunStep) Equals(r1 RunStep) bool {
return r.agentName == r1.agentName
}
func (r *RunStep) GobEncode() ([]byte, error) {
s := &runStepSerialization{AgentName: r.agentName}
buf := &bytes.Buffer{}
err := gob.NewEncoder(buf).Encode(s)
if err != nil {
return nil, fmt.Errorf("failed to gob encode RunStep: %w", err)
}
return buf.Bytes(), nil
}
func (r *RunStep) GobDecode(b []byte) error {
s := &runStepSerialization{}
err := gob.NewDecoder(bytes.NewReader(b)).Decode(s)
if err != nil {
return fmt.Errorf("failed to gob decode RunStep: %w", err)
}
r.agentName = s.AgentName
return nil
}
type runStepSerialization struct {
AgentName string
}
// TypedAgentEvent represents a single event emitted during agent execution.
// CheckpointSchema: persisted via serialization.RunCtx (gob).
type TypedAgentEvent[M MessageType] struct {
AgentName string
// RunPath represents the execution path from root agent to the current event source.
// This field is managed entirely by the framework and cannot be set by end-users.
//
// NOT RECOMMENDED: RunPath is mainly relevant for agent transfer and workflow agents,
// which have not proven to be more effective empirically. For ChatModelAgent with
// AgentTool or DeepAgent, RunPath is trivial. Consider those patterns instead.
RunPath []RunStep
Output *TypedAgentOutput[M]
Action *AgentAction
Err error
}
// AgentEvent is the default event type using *schema.Message.
type AgentEvent = TypedAgentEvent[*schema.Message]
type TypedAgentInput[M MessageType] struct {
Messages []M
EnableStreaming bool
}
type AgentInput = TypedAgentInput[*schema.Message]
// TypedAgent is the base agent interface parameterized by message type.
//
// For M = *schema.Message, the full ADK feature set is supported (multi-agent
// orchestration, cancel monitoring, retry, flowAgent).
// For M = *schema.AgenticMessage, single-agent execution works but cancel
// monitoring on the model stream and retry are not yet wired.
type TypedAgent[M MessageType] interface {
Name(ctx context.Context) string
Description(ctx context.Context) string
// Run runs the agent.
// The returned AgentEvent within the AsyncIterator must be safe to modify.
// If the returned AgentEvent within the AsyncIterator contains MessageStream,
// the MessageStream MUST be exclusive and safe to be received directly.
// NOTE: it's recommended to use SetAutomaticClose() on the MessageStream of AgentEvents emitted by AsyncIterator,
// so that even the events are not processed, the MessageStream can still be closed.
Run(ctx context.Context, input *TypedAgentInput[M], options ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]]
}
//go:generate mockgen -destination ../internal/mock/adk/Agent_mock.go --package adk github.com/cloudwego/eino/adk Agent,ResumableAgent
type Agent = TypedAgent[*schema.Message]
// OnSubAgents is the interface for agents that support sub-agent registration and transfer.
//
// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven
// to be more effective empirically. Consider using ChatModelAgent with AgentTool
// or DeepAgent instead for most multi-agent scenarios.
type OnSubAgents interface {
OnSetSubAgents(ctx context.Context, subAgents []Agent) error
OnSetAsSubAgent(ctx context.Context, parent Agent) error
OnDisallowTransferToParent(ctx context.Context) error
}
type TypedResumableAgent[M MessageType] interface {
TypedAgent[M]
Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]]
}
type ResumableAgent = TypedResumableAgent[*schema.Message]
func concatMessageStream[M MessageType](stream *schema.StreamReader[M]) (M, error) {
var zero M
switch s := any(stream).(type) {
case *schema.StreamReader[*schema.Message]:
result, err := schema.ConcatMessageStream(s)
if err != nil {
return zero, err
}
return any(result).(M), nil
case *schema.StreamReader[*schema.AgenticMessage]:
defer s.Close()
var msgs []*schema.AgenticMessage
for {
frame, err := s.Recv()
if err == io.EOF {
break
}
if err != nil {
return zero, err
}
msgs = append(msgs, frame)
}
result, err := schema.ConcatAgenticMessages(msgs)
if err != nil {
return zero, err
}
return any(result).(M), nil
default:
panic("unreachable: unknown MessageType")
}
}