520 lines
18 KiB
Go
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")
|
|
}
|
|
}
|