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

1715 lines
58 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"
"errors"
"fmt"
"math"
"runtime/debug"
"strings"
"sync"
"sync/atomic"
"github.com/bytedance/sonic"
"github.com/cloudwego/eino/adk/internal"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/components/prompt"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/internal/safe"
"github.com/cloudwego/eino/schema"
)
var _ ResumableAgent = &TypedChatModelAgent[*schema.Message]{}
var _ TypedResumableAgent[*schema.AgenticMessage] = &TypedChatModelAgent[*schema.AgenticMessage]{}
type typedChatModelAgentExecCtx[M MessageType] struct {
runtimeReturnDirectly map[string]bool
generator *AsyncGenerator[*TypedAgentEvent[M]]
cancelCtx *cancelContext
failoverLastSuccessModel model.BaseModel[M]
// suppressEventSend prevents eventSenderModel from emitting AgentEvents for the current
// Generate call. Set to true before each rejected retry attempt and reset to false after.
// Invariant: any code path that emits model output events MUST check this flag.
suppressEventSend bool
retryVerdictSignal *retryVerdictSignal
afterToolCallsHook func(ctx context.Context) error
}
func (e *typedChatModelAgentExecCtx[M]) send(event *TypedAgentEvent[M]) {
if e == nil || e.generator == nil {
return
}
if e.cancelCtx != nil && e.cancelCtx.isImmediateCancelled() {
return
}
e.generator.trySend(event)
}
type chatModelAgentExecCtx = typedChatModelAgentExecCtx[*schema.Message]
type typedChatModelAgentExecCtxKey[M MessageType] struct{}
func withTypedChatModelAgentExecCtx[M MessageType](ctx context.Context, execCtx *typedChatModelAgentExecCtx[M]) context.Context {
return context.WithValue(ctx, typedChatModelAgentExecCtxKey[M]{}, execCtx)
}
func getTypedChatModelAgentExecCtx[M MessageType](ctx context.Context) *typedChatModelAgentExecCtx[M] {
if v := ctx.Value(typedChatModelAgentExecCtxKey[M]{}); v != nil {
return v.(*typedChatModelAgentExecCtx[M])
}
return nil
}
type chatModelAgentRunOptions struct {
chatModelOptions []model.Option
toolOptions []tool.Option
agentToolOptions map[string][]AgentRunOption
historyModifier func(context.Context, []Message) []Message
afterToolCallsHook func(ctx context.Context) error
}
// WithChatModelOptions sets options for the underlying chat model.
func WithChatModelOptions(opts []model.Option) AgentRunOption {
return WrapImplSpecificOptFn(func(t *chatModelAgentRunOptions) {
t.chatModelOptions = opts
})
}
// WithToolOptions sets options for tools used by the chat model agent.
func WithToolOptions(opts []tool.Option) AgentRunOption {
return WrapImplSpecificOptFn(func(t *chatModelAgentRunOptions) {
t.toolOptions = opts
})
}
// WithAgentToolRunOptions specifies per-tool run options for the agent.
func WithAgentToolRunOptions(opts map[string][]AgentRunOption) AgentRunOption {
return WrapImplSpecificOptFn(func(t *chatModelAgentRunOptions) {
t.agentToolOptions = opts
})
}
// WithHistoryModifier sets a function to modify history during resume.
// Deprecated: use ResumeWithData and ChatModelAgentResumeData instead.
func WithHistoryModifier(f func(context.Context, []Message) []Message) AgentRunOption {
return WrapImplSpecificOptFn(func(t *chatModelAgentRunOptions) {
t.historyModifier = f
})
}
// WithAfterToolCallsHook registers a per-run hook that fires synchronously after
// all tool calls in a react iteration complete, before the next ChatModel call.
//
// This is suitable for TurnLoop Push+Preempt patterns where the pushed item
// must be visible to the next turn's GenInput.
func WithAfterToolCallsHook(fn func(ctx context.Context) error) AgentRunOption {
return WrapImplSpecificOptFn(func(t *chatModelAgentRunOptions) {
t.afterToolCallsHook = fn
})
}
type ToolsConfig struct {
compose.ToolsNodeConfig
// ReturnDirectly specifies tools that cause the agent to return immediately when called.
// The map keys are tool names indicate whether the tool should trigger immediate return.
ReturnDirectly map[string]bool
// EmitInternalEvents indicates whether internal events from agentTool should be emitted
// to the parent agent's AsyncGenerator, allowing real-time streaming of nested agent output
// to the end-user via Runner.
//
// Note that these forwarded events are NOT recorded in the parent agent's runSession.
// They are only emitted to the end-user and have no effect on the parent agent's state
// or checkpoint.
//
// Action Scoping:
// Actions emitted by the inner agent are scoped to the agent tool boundary:
// - Interrupted: Propagated via CompositeInterrupt to allow proper interrupt/resume
// - Exit, TransferToAgent, BreakLoop: Ignored outside the agent tool
EmitInternalEvents bool
}
// TypedGenModelInput transforms the agent's system instruction and user input into model input
// messages ([]M). This is the primary customization point for controlling what the model sees.
// The default implementation prepends a system message (if instruction is non-empty),
// followed by the user's input messages.
type TypedGenModelInput[M MessageType] func(ctx context.Context, instruction string, input *TypedAgentInput[M]) ([]M, error)
// GenModelInput transforms agent instructions and input into a format suitable for the model.
type GenModelInput = TypedGenModelInput[*schema.Message]
func defaultGenModelInput(ctx context.Context, instruction string, input *AgentInput) ([]Message, error) {
msgs := make([]Message, 0, len(input.Messages)+1)
if instruction != "" {
sp := schema.SystemMessage(instruction)
vs := GetSessionValues(ctx)
if len(vs) > 0 {
ct := prompt.FromMessages(schema.FString, sp)
ms, err := ct.Format(ctx, vs)
if err != nil {
return nil, fmt.Errorf("defaultGenModelInput: failed to format instruction using FString template. "+
"This formatting is triggered automatically when SessionValues are present. "+
"If your instruction contains literal curly braces (e.g., JSON), provide a custom GenModelInput that uses another format. If you are using "+
"SessionValues for purposes other than instruction formatting, provide a custom GenModelInput that does no formatting at all: %w", err)
}
sp = ms[0]
}
msgs = append(msgs, sp)
}
msgs = append(msgs, input.Messages...)
return msgs, nil
}
func newDefaultGenModelInput[M MessageType]() TypedGenModelInput[M] {
var zero M
switch any(zero).(type) {
case *schema.Message:
return any(GenModelInput(defaultGenModelInput)).(TypedGenModelInput[M])
case *schema.AgenticMessage:
return any(TypedGenModelInput[*schema.AgenticMessage](func(_ context.Context, instruction string, input *TypedAgentInput[*schema.AgenticMessage]) ([]*schema.AgenticMessage, error) {
msgs := make([]*schema.AgenticMessage, 0, len(input.Messages)+1)
if instruction != "" {
msgs = append(msgs, schema.SystemAgenticMessage(instruction))
}
msgs = append(msgs, input.Messages...)
return msgs, nil
})).(TypedGenModelInput[M])
default:
panic("unreachable: unknown MessageType")
}
}
// TypedChatModelAgentState represents the state of a chat model agent during conversation.
// This is the primary state type for both TypedChatModelAgentMiddleware and AgentMiddleware callbacks.
type TypedChatModelAgentState[M MessageType] struct {
// Messages contains all messages in the current conversation session.
Messages []M
// ToolInfos contains the tool definitions passed to the model via model.WithTools.
// BeforeModelRewriteState handlers can read and modify this field to control which tools
// the model sees on each call.
ToolInfos []*schema.ToolInfo
// DeferredToolInfos contains tool definitions for server-side deferred retrieval,
// passed to the model via model.WithDeferredTools. These tools are not included in the
// immediate tool list but can be discovered by the model through its native search capability.
// Nil when not in use.
DeferredToolInfos []*schema.ToolInfo
}
// ChatModelAgentState is the default state type using *schema.Message.
type ChatModelAgentState = TypedChatModelAgentState[*schema.Message]
// Deprecated: Use ChatModelAgentMiddleware (interface-based Handlers) instead.
// AgentMiddleware will be removed in a future release.
//
// AgentMiddleware provides hooks to customize agent behavior at various stages of execution.
type AgentMiddleware struct {
// AdditionalInstruction adds supplementary text to the agent's system instruction.
// This instruction is concatenated with the base instruction before each chat model call.
AdditionalInstruction string
// AdditionalTools adds supplementary tools to the agent's available toolset.
// These tools are combined with the tools configured for the agent.
AdditionalTools []tool.BaseTool
// BeforeChatModel is called before each ChatModel invocation, allowing modification of the agent state.
BeforeChatModel func(context.Context, *ChatModelAgentState) error
// AfterChatModel is called after each ChatModel invocation, allowing modification of the agent state.
AfterChatModel func(context.Context, *ChatModelAgentState) error
// WrapToolCall wraps tool calls with custom middleware logic.
// Each middleware contains Invokable and/or Streamable functions for tool calls.
WrapToolCall compose.ToolMiddleware
}
// TypedChatModelAgentConfig is the generic configuration for ChatModelAgent.
type TypedChatModelAgentConfig[M MessageType] struct {
// Name of the agent. Better be unique across all agents.
// Optional. If empty, the agent can still run standalone but cannot be used as
// a sub-agent tool via NewAgentTool (which requires a non-empty Name).
Name string
// Description of the agent's capabilities.
// Helps other agents determine whether to transfer tasks to this agent.
// Optional. If empty, the agent can still run standalone but cannot be used as
// a sub-agent tool via NewAgentTool (which requires a non-empty Description).
Description string
// Instruction used as the system prompt for this agent.
// Optional. If empty, no system prompt will be used.
// Supports f-string placeholders for session values in default GenModelInput, for example:
// "You are a helpful assistant. The current time is {Time}. The current user is {User}."
// These placeholders will be replaced with session values for "Time" and "User".
Instruction string
// Model is the chat model used by the agent.
// If your ChatModelAgent uses any tools, this model must support the model.WithTools
// call option, as that's how ChatModelAgent configures the model with tool information.
Model model.BaseModel[M]
ToolsConfig ToolsConfig
// GenModelInput transforms instructions and input messages into the model's input format.
// Optional. Defaults to defaultGenModelInput which combines instruction and messages.
GenModelInput TypedGenModelInput[M]
// Exit defines the tool used to terminate the agent process.
// Optional. If nil, no Exit Action will be generated.
// You can use the provided 'ExitTool' implementation directly.
//
// 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.
Exit tool.BaseTool
// OutputKey stores the agent's response in the session.
// Optional. When set, stores output via AddSessionValue(ctx, outputKey, msg.Content).
//
// 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.
OutputKey string
// MaxIterations defines the upper limit of ChatModel generation cycles.
// The agent will terminate with an error if this limit is exceeded.
// Optional. Defaults to 20.
MaxIterations int
// Deprecated: Use Handlers instead. Middlewares will be removed in a future release.
// Handlers provides a more flexible interface-based approach for extending agent behavior.
Middlewares []AgentMiddleware
// Handlers configures interface-based handlers for extending agent behavior.
// Unlike Middlewares (struct-based), Handlers allow users to:
// - Add custom methods to their handler implementations
// - Return modified context from handler methods
// - Centralize configuration in struct fields instead of closures
//
// Handlers are processed after Middlewares, in registration order.
// See ChatModelAgentMiddleware documentation for when to use Handlers vs Middlewares.
//
// Execution Order (relative to AgentMiddleware and ToolsConfig):
//
// Model call lifecycle (outermost to innermost wrapper chain):
// 1. AgentMiddleware.BeforeChatModel (hook, runs before model call)
// 2. ChatModelAgentMiddleware.BeforeModelRewriteState (hook, can modify state before model call)
// 3. failoverModelWrapper (internal - failover between models, if configured)
// 4. retryModelWrapper (internal - retries on failure, if configured)
// 5. eventSenderModelWrapper (internal - sends model response events)
// 6. ChatModelAgentMiddleware.WrapModel (wrapper, first registered is outermost)
// 7. callbackInjectionModelWrapper (internal - injects callbacks if not enabled; when failover is enabled, this is handled per-model inside failoverProxyModel instead)
// 8. failoverProxyModel (internal - dispatches to selected failover model, if configured) / Model.Generate/Stream
// 9. ChatModelAgentMiddleware.AfterModelRewriteState (hook, can modify state after model call)
// 10. AgentMiddleware.AfterChatModel (hook, runs after model call)
//
// Custom Event Sender Position:
// By default, events are sent after all user middlewares (WrapModel) have processed the output,
// containing the modified messages. To send events with original (unmodified) output, pass
// NewEventSenderModelWrapper as a Handler after the modifying middleware:
//
// agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
// Handlers: []adk.ChatModelAgentMiddleware{
// myCustomHandler, // First registered = outermost wrapper
// adk.NewEventSenderModelWrapper(), // Last registered = innermost, events sent with original output
// },
// })
//
// Handler order: first registered is outermost. So [A, B, C] becomes A(B(C(model))).
// EventSenderModelWrapper sends events in post-processing, so placing it innermost
// means it receives the original model output before outer handlers modify it.
//
// When EventSenderModelWrapper is detected in Handlers, the framework skips
// the default event sender to avoid duplicate events.
//
// Tool call lifecycle (outermost to innermost):
// 1. eventSenderToolWrapper (internal ToolMiddleware - sends tool result events after all processing)
// 2. ToolsConfig.ToolCallMiddlewares (ToolMiddleware)
// 3. AgentMiddleware.WrapToolCall (ToolMiddleware)
// 4. ChatModelAgentMiddleware.WrapToolCall (wrapper, first registered is outermost)
// 5. callbackInjectedToolCall (internal - injects callbacks if tool doesn't handle them)
// 6. Tool.InvokableRun/StreamableRun
//
// Custom Tool Event Sender Position:
// By default, tool result events are emitted by an internal event sender placed before
// all user middlewares (outermost), so events reflect the fully processed tool output.
// To control exactly where in the handler chain tool events are emitted, pass
// NewEventSenderToolWrapper() as one of the Handlers. Its position determines which
// middlewares' effects are visible in the emitted event:
//
// agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
// Handlers: []adk.ChatModelAgentMiddleware{
// loggingHandler, // Outermost: sees event-sender output
// adk.NewEventSenderToolWrapper(), // Events reflect output from handlers below
// sanitizationHandler, // Innermost: runs first, modifies tool output
// },
// })
//
// Handler order: first registered is outermost. So [A, B, C] wraps as A(B(C(tool))).
// The event sender captures tool output in post-processing, so its position controls
// which handlers' modifications are included in the emitted events.
//
// When NewEventSenderToolWrapper is detected in Handlers, the framework skips
// the default event sender to avoid duplicate events.
//
// Tool List Modification:
//
// There are two ways to modify the tool list:
//
// 1. In BeforeAgent: Modify ChatModelAgentContext.Tools ([]tool.BaseTool) directly. This affects
// both the tool info list passed to ChatModel AND the actual tools available for
// execution. Changes persist for the entire agent run.
//
// 2. In BeforeModelRewriteState: Modify state.ToolInfos and state.DeferredToolInfos directly.
// This affects the tool info list passed to ChatModel for this and all subsequent model
// calls (changes are persisted in state). This is the recommended approach for dynamic
// tool filtering/selection based on conversation context.
//
// Modifying tools in WrapModel (e.g. via model.WithTools) is discouraged: changes there
// are NOT persisted in state, only affect a single model call, and break prompt cache.
Handlers []TypedChatModelAgentMiddleware[M]
// ModelRetryConfig configures retry behavior for the ChatModel.
// When set, the agent will automatically retry failed ChatModel calls
// based on the configured policy.
// Optional. If nil, no retry will be performed.
ModelRetryConfig *TypedModelRetryConfig[M]
// ModelFailoverConfig configures failover behavior for the ChatModel.
// When set, the agent will first try the last successful model (initially the configured Model),
// and on failure, call GetFailoverModel to select alternate models.
// Model field is still required as it serves as the initial model.
// Optional. If nil, no failover will be performed.
ModelFailoverConfig *ModelFailoverConfig[M]
}
type ChatModelAgentConfig = TypedChatModelAgentConfig[*schema.Message]
// TypedChatModelAgent is a chat model-backed agent parameterized by message type.
//
// For M = *schema.Message, the full ReAct loop (model → tool calls → model) is used.
// For M = *schema.AgenticMessage, a single-shot chain is used since agentic models
// handle tool calling internally. Cancel monitoring and retry on the model stream
// are not yet supported for agentic models.
type TypedChatModelAgent[M MessageType] struct {
name string
description string
instruction string
model model.BaseModel[M]
toolsConfig ToolsConfig
genModelInput TypedGenModelInput[M]
outputKey string
maxIterations int
subAgents []TypedAgent[M]
parentAgent TypedAgent[M]
disallowTransferToParent bool
exit tool.BaseTool
handlers []TypedChatModelAgentMiddleware[M]
middlewares []AgentMiddleware
modelRetryConfig *TypedModelRetryConfig[M]
modelFailoverConfig *ModelFailoverConfig[M]
once sync.Once
run typedRunFunc[M]
frozen uint32
exeCtx *execContext
}
type ChatModelAgent = TypedChatModelAgent[*schema.Message]
// typedRunParams holds the parameters for a typedRunFunc invocation.
type typedRunParams[M MessageType] struct {
input *TypedAgentInput[M]
generator *AsyncGenerator[*TypedAgentEvent[M]]
store *bridgeStore
instruction string
returnDirectly map[string]bool
cancelCtx *cancelContext
cancelCtxOwned bool
composeOpts []compose.Option
afterToolCallsHook func(ctx context.Context) error
}
type typedRunFunc[M MessageType] func(ctx context.Context, p *typedRunParams[M])
func resolveRunCancelContext(ctx context.Context, o *options) (*cancelContext, bool) {
inherited := getCancelContext(ctx)
if o.cancelCtx != nil {
return o.cancelCtx, o.cancelCtx != inherited
}
return inherited, false
}
// NewChatModelAgent creates a new ChatModelAgent with the given config.
func NewChatModelAgent(ctx context.Context, config *ChatModelAgentConfig) (*ChatModelAgent, error) {
return NewTypedChatModelAgent[*schema.Message](ctx, config)
}
// NewTypedChatModelAgent creates a new TypedChatModelAgent with the given config.
func NewTypedChatModelAgent[M MessageType](ctx context.Context, config *TypedChatModelAgentConfig[M]) (*TypedChatModelAgent[M], error) {
if config.ModelFailoverConfig != nil {
if config.ModelFailoverConfig.GetFailoverModel == nil {
return nil, errors.New("ModelFailoverConfig.GetFailoverModel is required when ModelFailoverConfig is set")
}
// ShouldFailover is required when ModelFailoverConfig is set
if config.ModelFailoverConfig.ShouldFailover == nil {
return nil, errors.New("ModelFailoverConfig.ShouldFailover is required when ModelFailoverConfig is set")
}
}
if config.Model == nil {
return nil, errors.New("agent 'Model' is required")
}
var genInput TypedGenModelInput[M]
if config.GenModelInput != nil {
genInput = config.GenModelInput
} else {
genInput = newDefaultGenModelInput[M]()
}
tc := config.ToolsConfig
// Tool call middleware execution order (outermost to innermost):
// 1. eventSenderToolWrapper (internal - sends tool result events after all modifications)
// 2. User-provided ToolsConfig.ToolCallMiddlewares (original order preserved)
// 3. Middlewares' WrapToolCall (in registration order)
// 4. ChatModelAgentMiddleware.WrapToolCall (in registration order)
// 5. callbackInjectedToolCall (internal - injects callbacks if tool doesn't handle them)
if !hasUserEventSenderToolWrapper(config.Handlers) {
defaultToolEventSender := handlersToToolMiddlewares([]TypedChatModelAgentMiddleware[M]{newTypedEventSenderToolWrapper[M]()})
tc.ToolCallMiddlewares = append(defaultToolEventSender, tc.ToolCallMiddlewares...)
}
tc.ToolCallMiddlewares = append(tc.ToolCallMiddlewares, collectToolMiddlewaresFromMiddlewares(config.Middlewares)...)
// Cancel monitoring middleware (innermost — close to the tool endpoint).
// This allows early abort of the raw tool result stream when immediateChan fires
// (CancelImmediate or timeout escalation), while requiring outer wrappers to
// propagate stream errors such as ErrStreamCanceled without swallowing them.
cancelToolHandler := &cancelMonitoredToolHandler{}
tc.ToolCallMiddlewares = append(tc.ToolCallMiddlewares, compose.ToolMiddleware{
Streamable: cancelToolHandler.WrapStreamableToolCall,
EnhancedStreamable: cancelToolHandler.WrapEnhancedStreamableToolCall,
})
return &TypedChatModelAgent[M]{
name: config.Name,
description: config.Description,
instruction: config.Instruction,
model: config.Model,
toolsConfig: tc,
genModelInput: genInput,
exit: config.Exit,
outputKey: config.OutputKey,
maxIterations: config.MaxIterations,
handlers: config.Handlers,
middlewares: config.Middlewares,
modelRetryConfig: config.ModelRetryConfig,
modelFailoverConfig: config.ModelFailoverConfig,
}, nil
}
func collectToolMiddlewaresFromMiddlewares(mws []AgentMiddleware) []compose.ToolMiddleware {
var middlewares []compose.ToolMiddleware
for _, m := range mws {
if m.WrapToolCall.Invokable == nil && m.WrapToolCall.Streamable == nil && m.WrapToolCall.EnhancedStreamable == nil && m.WrapToolCall.EnhancedInvokable == nil {
continue
}
middlewares = append(middlewares, m.WrapToolCall)
}
return middlewares
}
const (
TransferToAgentToolName = "transfer_to_agent"
TransferToAgentToolDesc = "Transfer the question to another agent."
TransferToAgentToolDescChinese = "将问题移交给其他 Agent。"
)
var (
toolInfoTransferToAgent = &schema.ToolInfo{
Name: TransferToAgentToolName,
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"agent_name": {
Desc: "the name of the agent to transfer to",
Required: true,
Type: schema.String,
},
}),
}
ToolInfoExit = &schema.ToolInfo{
Name: "exit",
Desc: "Exit the agent process and return the final result.",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"final_result": {
Desc: "the final result to return",
Required: true,
Type: schema.String,
},
}),
}
)
type ExitTool struct{}
func (et ExitTool) Info(_ context.Context) (*schema.ToolInfo, error) {
return ToolInfoExit, nil
}
func (et ExitTool) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) {
type exitParams struct {
FinalResult string `json:"final_result"`
}
params := &exitParams{}
err := sonic.UnmarshalString(argumentsInJSON, params)
if err != nil {
return "", err
}
err = SendToolGenAction(ctx, "exit", NewExitAction())
if err != nil {
return "", err
}
return params.FinalResult, nil
}
type transferToAgent struct{}
func (tta transferToAgent) Info(_ context.Context) (*schema.ToolInfo, error) {
desc := internal.SelectPrompt(internal.I18nPrompts{
English: TransferToAgentToolDesc,
Chinese: TransferToAgentToolDescChinese,
})
info := *toolInfoTransferToAgent
info.Desc = desc
return &info, nil
}
func transferToAgentToolOutput(destName string) string {
tpl := internal.SelectPrompt(internal.I18nPrompts{
English: "successfully transferred to agent [%s]",
Chinese: "成功移交任务至 agent [%s]",
})
return fmt.Sprintf(tpl, destName)
}
func (tta transferToAgent) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) {
type transferParams struct {
AgentName string `json:"agent_name"`
}
params := &transferParams{}
err := sonic.UnmarshalString(argumentsInJSON, params)
if err != nil {
return "", err
}
err = SendToolGenAction(ctx, TransferToAgentToolName, NewTransferToAgentAction(params.AgentName))
if err != nil {
return "", err
}
return transferToAgentToolOutput(params.AgentName), nil
}
func (a *TypedChatModelAgent[M]) Name(_ context.Context) string {
return a.name
}
func (a *TypedChatModelAgent[M]) Description(_ context.Context) string {
return a.description
}
func (a *TypedChatModelAgent[M]) GetType() string {
return "ChatModel"
}
// OnSetSubAgents implements OnSubAgents.
//
// 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 (a *TypedChatModelAgent[M]) OnSetSubAgents(_ context.Context, subAgents []TypedAgent[M]) error {
if atomic.LoadUint32(&a.frozen) == 1 {
return errors.New("agent has been frozen after run")
}
if len(a.subAgents) > 0 {
return errors.New("agent's sub-agents has already been set")
}
a.subAgents = subAgents
return nil
}
// OnSetAsSubAgent implements OnSubAgents.
//
// 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 (a *TypedChatModelAgent[M]) OnSetAsSubAgent(_ context.Context, parent TypedAgent[M]) error {
if atomic.LoadUint32(&a.frozen) == 1 {
return errors.New("agent has been frozen after run")
}
if a.parentAgent != nil {
return errors.New("agent has already been set as a sub-agent of another agent")
}
a.parentAgent = parent
return nil
}
// OnDisallowTransferToParent implements OnSubAgents.
//
// 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 (a *TypedChatModelAgent[M]) OnDisallowTransferToParent(_ context.Context) error {
if atomic.LoadUint32(&a.frozen) == 1 {
return errors.New("agent has been frozen after run")
}
a.disallowTransferToParent = true
return nil
}
type ChatModelAgentInterruptInfo struct {
Info *compose.InterruptInfo
Data []byte
}
func init() {
schema.RegisterName[*ChatModelAgentInterruptInfo]("_eino_adk_chat_model_agent_interrupt_info")
}
func extractTextContent[M MessageType](msg M) string {
switch v := any(msg).(type) {
case *schema.Message:
return v.Content
case *schema.AgenticMessage:
var texts []string
for _, block := range v.ContentBlocks {
if block != nil && block.Type == schema.ContentBlockTypeAssistantGenText && block.AssistantGenText != nil {
texts = append(texts, block.AssistantGenText.Text)
}
}
return strings.Join(texts, "\n")
default:
return ""
}
}
func setOutputToSession[M MessageType](ctx context.Context, msg M, msgStream *schema.StreamReader[M], outputKey string) error {
if !isNilMessage(msg) {
AddSessionValue(ctx, outputKey, extractTextContent(msg))
return nil
}
concatenated, err := concatMessageStream(msgStream)
if err != nil {
return err
}
AddSessionValue(ctx, outputKey, extractTextContent(concatenated))
return nil
}
func typedErrFunc[M MessageType](err error) typedRunFunc[M] {
return func(ctx context.Context, p *typedRunParams[M]) {
p.generator.Send(&TypedAgentEvent[M]{Err: err})
}
}
// ChatModelAgentResumeData holds data that can be provided to a ChatModelAgent during a resume operation
// to modify its behavior. It is provided via the adk.ResumeWithData function.
type ChatModelAgentResumeData struct {
// HistoryModifier is a function that can transform the agent's message history before it is sent to the model.
// This allows for adding new information or context upon resumption.
HistoryModifier func(ctx context.Context, history []Message) []Message
}
type execContext struct {
instruction string
toolsNodeConf compose.ToolsNodeConfig
returnDirectly map[string]bool
toolInfos []*schema.ToolInfo
unwrappedTools []tool.BaseTool
toolSearchTool *schema.ToolInfo // set by BeforeAgent when the model supports native tool search
rebuildGraph bool // whether needs to instantiate a new graph because of topology changes due to tool modifications
toolUpdated bool // whether needs to pass a compose.WithToolList option to ToolsNode due to tool list change
}
func (a *TypedChatModelAgent[M]) applyBeforeAgent(ctx context.Context, ec *execContext) (context.Context, *execContext, error) {
runCtx := &ChatModelAgentContext{
Instruction: ec.instruction,
Tools: cloneSlice(ec.unwrappedTools),
ReturnDirectly: copyMap(ec.returnDirectly),
}
var err error
for i, handler := range a.handlers {
ctx, runCtx, err = handler.BeforeAgent(ctx, runCtx)
if err != nil {
return ctx, nil, fmt.Errorf("handler[%d] (%T) BeforeAgent failed: %w", i, handler, err)
}
}
toolsNodeConf := ec.toolsNodeConf
toolsNodeConf.Tools = runCtx.Tools
toolsNodeConf.ToolCallMiddlewares = cloneSlice(ec.toolsNodeConf.ToolCallMiddlewares)
runtimeEC := &execContext{
instruction: runCtx.Instruction,
toolsNodeConf: toolsNodeConf,
returnDirectly: runCtx.ReturnDirectly,
toolSearchTool: runCtx.ToolSearchTool,
toolUpdated: true,
rebuildGraph: (len(ec.toolsNodeConf.Tools) == 0 && len(runCtx.Tools) > 0) ||
(len(ec.returnDirectly) == 0 && len(runCtx.ReturnDirectly) > 0),
}
toolInfos, err := genToolInfos(ctx, &runtimeEC.toolsNodeConf)
if err != nil {
return ctx, nil, err
}
runtimeEC.toolInfos = toolInfos
return ctx, runtimeEC, nil
}
func (a *TypedChatModelAgent[M]) applyAfterAgent(ctx context.Context) (context.Context, error) {
if len(a.handlers) == 0 {
return ctx, nil
}
var state TypedChatModelAgentState[M]
_ = compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error {
state.Messages = st.Messages
state.ToolInfos = st.ToolInfos
state.DeferredToolInfos = st.DeferredToolInfos
return nil
})
var err error
for i, handler := range a.handlers {
ctx, err = handler.AfterAgent(ctx, &state)
if err != nil {
return ctx, fmt.Errorf("handler[%d] (%T) AfterAgent failed: %w", i, handler, err)
}
}
return ctx, nil
}
func (a *TypedChatModelAgent[M]) prepareExecContext(ctx context.Context) (*execContext, error) {
instruction := a.instruction
toolsNodeConf := a.toolsConfig.ToolsNodeConfig
toolsNodeConf.Tools = cloneSlice(a.toolsConfig.Tools)
toolsNodeConf.ToolCallMiddlewares = cloneSlice(a.toolsConfig.ToolCallMiddlewares)
returnDirectly := copyMap(a.toolsConfig.ReturnDirectly)
transferToAgents := a.subAgents
if a.parentAgent != nil && !a.disallowTransferToParent {
transferToAgents = append(transferToAgents, a.parentAgent)
}
if len(transferToAgents) > 0 {
transferInstruction := genTransferToAgentInstruction(ctx, transferToAgents)
instruction = concatInstructions(instruction, transferInstruction)
toolsNodeConf.Tools = append(toolsNodeConf.Tools, &transferToAgent{})
returnDirectly[TransferToAgentToolName] = true
}
if a.exit != nil {
toolsNodeConf.Tools = append(toolsNodeConf.Tools, a.exit)
exitInfo, err := a.exit.Info(ctx)
if err != nil {
return nil, err
}
returnDirectly[exitInfo.Name] = true
}
for _, m := range a.middlewares {
if m.AdditionalInstruction != "" {
instruction = concatInstructions(instruction, m.AdditionalInstruction)
}
toolsNodeConf.Tools = append(toolsNodeConf.Tools, m.AdditionalTools...)
}
unwrappedTools := cloneSlice(toolsNodeConf.Tools)
handlerMiddlewares := handlersToToolMiddlewares(a.handlers)
toolsNodeConf.ToolCallMiddlewares = append(toolsNodeConf.ToolCallMiddlewares, handlerMiddlewares...)
toolInfos, err := genToolInfos(ctx, &toolsNodeConf)
if err != nil {
return nil, err
}
return &execContext{
instruction: instruction,
toolsNodeConf: toolsNodeConf,
returnDirectly: returnDirectly,
toolInfos: toolInfos,
unwrappedTools: unwrappedTools,
}, nil
}
// handleRunFuncError is the common error handler for buildNoToolsRunFunc and buildReActRunFunc.
// It handles compose interrupts (both cancel-triggered and business)
// and generic errors, sending the appropriate event to the generator.
func (a *TypedChatModelAgent[M]) handleRunFuncError(
ctx context.Context,
err error,
cancelCtx *cancelContext,
cancelCtxOwned bool,
store *bridgeStore,
generator *AsyncGenerator[*TypedAgentEvent[M]],
) {
info, ok := compose.ExtractInterruptInfo(err)
if ok {
if cancelCtx != nil {
if !cancelCtx.shouldCancel() {
// Note: there is a benign TOCTOU window here. Between shouldCancel()
// returning false and markDone() executing, a concurrent cancel could
// transition stateRunning→stateCancelling. markDone() then does
// stateCancelling→stateDone, and the cancel func receives
// ErrExecutionEnded (execution finished before cancel took effect).
cancelCtx.markDone()
}
}
data, existed, sErr := store.Get(ctx, bridgeCheckpointID)
if sErr != nil {
generator.Send(&TypedAgentEvent[M]{AgentName: a.name, Err: fmt.Errorf("failed to get interrupt info: %w", sErr)})
return
}
if !existed {
generator.Send(&TypedAgentEvent[M]{AgentName: a.name, Err: fmt.Errorf("interrupt occurred but checkpoint data is missing")})
return
}
is := FromInterruptContexts(info.InterruptContexts)
event := TypedCompositeInterrupt[M](ctx, info, data, is)
event.Action.Interrupted.Data = &ChatModelAgentInterruptInfo{
Info: info,
Data: data,
}
event.AgentName = a.name
generator.Send(event)
return
}
if cancelCtxOwned && cancelCtx != nil {
cancelCtx.markDone()
}
generator.Send(&TypedAgentEvent[M]{Err: err})
}
type typedNoToolsInput[M MessageType] struct {
input *TypedAgentInput[M]
instruction string
}
func appendModelToChain[I, O any, M MessageType](chain *compose.Chain[I, O], m model.BaseModel[M]) {
var zero M
switch any(zero).(type) {
case *schema.Message:
chain.AppendChatModel(any(m).(model.BaseChatModel))
case *schema.AgenticMessage:
chain.AppendAgenticModel(any(m).(model.AgenticModel))
}
}
func (a *TypedChatModelAgent[M]) buildNoToolsRunFunc(_ context.Context) (typedRunFunc[M], error) {
return func(ctx context.Context, p *typedRunParams[M]) {
cancelCtx := p.cancelCtx
ctx = withCancelContext(ctx, cancelCtx)
wrappedModel := buildModelWrappers(a.model, &typedModelWrapperConfig[M]{
handlers: a.handlers,
middlewares: a.middlewares,
retryConfig: a.modelRetryConfig,
failoverConfig: a.modelFailoverConfig,
cancelContext: cancelCtx,
})
chain := compose.NewChain[typedNoToolsInput[M], M](
compose.WithGenLocalState(func(ctx context.Context) (state *typedState[M]) {
return &typedState[M]{}
}))
chain.AppendLambda(compose.InvokableLambda(func(ctx context.Context, in typedNoToolsInput[M]) ([]M, error) {
messages, err := a.genModelInput(ctx, in.instruction, in.input)
if err != nil {
return nil, err
}
if err := compose.ProcessState(ctx, func(_ context.Context, st *typedState[M]) error {
st.Messages = append(st.Messages, messages...)
return nil
}); err != nil {
return nil, err
}
return messages, nil
}))
appendModelToChain(chain, wrappedModel)
if len(a.handlers) > 0 {
chain.AppendLambda(compose.InvokableLambda(func(ctx context.Context, msg M) (M, error) {
_, err := a.applyAfterAgent(ctx)
return msg, err
}))
}
var compileOptions []compose.GraphCompileOption
compileOptions = append(compileOptions,
compose.WithGraphName(a.name),
compose.WithCheckPointStore(p.store),
compose.WithSerializer(&gobSerializer{}))
if cancelCtx != nil {
var interrupt func(...compose.GraphInterruptOption)
ctx, interrupt = compose.WithGraphInterrupt(ctx)
cancelCtx.setGraphInterruptFunc(interrupt)
}
r, err := chain.Compile(ctx, compileOptions...)
if err != nil {
p.generator.Send(&TypedAgentEvent[M]{Err: err})
return
}
ctx = withTypedChatModelAgentExecCtx(ctx, &typedChatModelAgentExecCtx[M]{
generator: p.generator,
cancelCtx: cancelCtx,
failoverLastSuccessModel: a.model,
})
// Pre-execution cancel check
if cancelCtx != nil && cancelCtx.shouldCancel() {
if cancelCtx.getMode() == CancelImmediate || atomic.LoadInt32(&cancelCtx.escalated) == 1 {
cancelErr, ok := cancelCtx.createAndMarkCancelHandled()
if !ok {
return
}
p.generator.Send(&TypedAgentEvent[M]{Err: cancelErr})
return
}
}
in := typedNoToolsInput[M]{input: p.input, instruction: p.instruction}
var msg M
var msgStream *schema.StreamReader[M]
if p.input.EnableStreaming {
msgStream, err = r.Stream(ctx, in, p.composeOpts...)
} else {
msg, err = r.Invoke(ctx, in, p.composeOpts...)
}
if err == nil {
if a.outputKey != "" {
err = setOutputToSession(ctx, msg, msgStream, a.outputKey)
if err != nil {
p.generator.Send(&TypedAgentEvent[M]{Err: err})
}
} else if msgStream != nil {
msgStream.Close()
}
return
}
a.handleRunFuncError(ctx, err, cancelCtx, p.cancelCtxOwned, p.store, p.generator)
}, nil
}
func (a *TypedChatModelAgent[M]) buildReActRunFunc(ctx context.Context, bc *execContext) (typedRunFunc[M], error) {
var zero M
switch any(zero).(type) {
case *schema.Message:
return a.buildMessageReActRunFunc(ctx, bc)
case *schema.AgenticMessage:
// single-shot: agentic models handle tool calling internally
return a.buildAgenticReActRunFunc(ctx, bc)
default:
return nil, fmt.Errorf("unsupported message type %T for ReAct run mode", zero)
}
}
type reactRunInput struct {
input *AgentInput
instruction string
}
func (a *TypedChatModelAgent[M]) buildMessageReActRunFunc(_ context.Context, bc *execContext) (typedRunFunc[M], error) {
// safe: only called when M = *schema.Message (guarded by type switch in buildReActRunFunc)
msgModel := any(a.model).(model.BaseChatModel)
msgHandlers := any(a.handlers).([]ChatModelAgentMiddleware)
genModelInputFn := any(a.genModelInput).(GenModelInput)
msgConf := &reactConfig{
model: msgModel,
toolsConfig: &bc.toolsNodeConf,
modelWrapperConf: &modelWrapperConfig{
handlers: msgHandlers,
middlewares: a.middlewares,
retryConfig: any(a.modelRetryConfig).(*ModelRetryConfig),
failoverConfig: any(a.modelFailoverConfig).(*ModelFailoverConfig[*schema.Message]),
toolInfos: bc.toolInfos,
},
toolsReturnDirectly: bc.returnDirectly,
agentName: a.name,
maxIterations: a.maxIterations,
}
if len(a.handlers) > 0 {
msgAgent := any(a).(*TypedChatModelAgent[*schema.Message])
msgConf.afterAgentFunc = func(ctx context.Context, msg *schema.Message) (*schema.Message, error) {
_, err := msgAgent.applyAfterAgent(ctx)
return msg, err
}
}
return func(ctx context.Context, p *typedRunParams[M]) {
mp := any(p).(*typedRunParams[*schema.Message])
cancelCtx := mp.cancelCtx
msgConf.cancelCtx = cancelCtx
if msgConf.modelWrapperConf != nil {
msgConf.modelWrapperConf.cancelContext = cancelCtx
}
ctx = withCancelContext(ctx, cancelCtx)
g, err := newReact(ctx, msgConf)
if err != nil {
mp.generator.Send(&AgentEvent{Err: err})
return
}
chain := compose.NewChain[reactRunInput, Message]().
AppendLambda(
compose.InvokableLambda(func(ctx context.Context, in reactRunInput) (*reactInput, error) {
messages, genErr := genModelInputFn(ctx, in.instruction, in.input)
if genErr != nil {
return nil, genErr
}
return &reactInput{
Messages: messages,
}, nil
}),
).
AppendGraph(g, compose.WithNodeName("ReAct"), compose.WithGraphCompileOptions(compose.WithMaxRunSteps(math.MaxInt)))
var compileOptions []compose.GraphCompileOption
compileOptions = append(compileOptions,
compose.WithGraphName(a.name),
compose.WithCheckPointStore(mp.store),
compose.WithSerializer(&gobSerializer{}),
compose.WithMaxRunSteps(math.MaxInt))
if cancelCtx != nil {
var interrupt func(...compose.GraphInterruptOption)
ctx, interrupt = compose.WithGraphInterrupt(ctx)
cancelCtx.setGraphInterruptFunc(interrupt)
}
runnable, err_ := chain.Compile(ctx, compileOptions...)
if err_ != nil {
mp.generator.Send(&AgentEvent{Err: err_})
return
}
ctx = withTypedChatModelAgentExecCtx(ctx, &chatModelAgentExecCtx{
runtimeReturnDirectly: mp.returnDirectly,
generator: mp.generator,
cancelCtx: cancelCtx,
failoverLastSuccessModel: msgModel,
afterToolCallsHook: mp.afterToolCallsHook,
})
// Pre-execution cancel check
if cancelCtx != nil && cancelCtx.shouldCancel() {
if cancelCtx.getMode() == CancelImmediate || atomic.LoadInt32(&cancelCtx.escalated) == 1 {
cancelErr, ok := cancelCtx.createAndMarkCancelHandled()
if !ok {
return
}
mp.generator.Send(&AgentEvent{Err: cancelErr})
return
}
}
in := reactRunInput{
input: mp.input,
instruction: mp.instruction,
}
var runOpts []compose.Option
runOpts = append(runOpts, mp.composeOpts...)
if a.toolsConfig.EmitInternalEvents {
runOpts = append(runOpts, compose.WithToolsNodeOption(compose.WithToolOption(withAgentToolEventGenerator(mp.generator))))
}
if mp.input.EnableStreaming {
runOpts = append(runOpts, compose.WithToolsNodeOption(compose.WithToolOption(withAgentToolEnableStreaming(true))))
}
var msg Message
var msgStream MessageStream
if mp.input.EnableStreaming {
msgStream, err_ = runnable.Stream(ctx, in, runOpts...)
} else {
msg, err_ = runnable.Invoke(ctx, in, runOpts...)
}
if err_ == nil {
if a.outputKey != "" {
err_ = setOutputToSession[*schema.Message](ctx, msg, msgStream, a.outputKey)
if err_ != nil {
mp.generator.Send(&AgentEvent{Err: err_})
}
} else if msgStream != nil {
msgStream.Close()
}
return
}
a.handleRunFuncError(ctx, err_, cancelCtx, mp.cancelCtxOwned, mp.store, p.generator)
}, nil
}
type agenticReactRunInput struct {
input *TypedAgentInput[*schema.AgenticMessage]
instruction string
}
func (a *TypedChatModelAgent[M]) buildAgenticReActRunFunc(_ context.Context, bc *execContext) (typedRunFunc[M], error) {
agenticModel := any(a.model).(model.AgenticModel)
agenticHandlers := any(a.handlers).([]TypedChatModelAgentMiddleware[*schema.AgenticMessage])
genModelInputFn := any(a.genModelInput).(TypedGenModelInput[*schema.AgenticMessage])
agenticConf := &agenticReactConfig{
model: agenticModel,
toolsConfig: &bc.toolsNodeConf,
modelWrapperConf: &typedModelWrapperConfig[*schema.AgenticMessage]{
handlers: agenticHandlers,
middlewares: a.middlewares,
retryConfig: any(a.modelRetryConfig).(*TypedModelRetryConfig[*schema.AgenticMessage]),
failoverConfig: any(a.modelFailoverConfig).(*ModelFailoverConfig[*schema.AgenticMessage]),
toolInfos: bc.toolInfos,
},
toolsReturnDirectly: bc.returnDirectly,
agentName: a.name,
maxIterations: a.maxIterations,
}
if len(a.handlers) > 0 {
agenticAgent := any(a).(*TypedChatModelAgent[*schema.AgenticMessage])
agenticConf.afterAgentFunc = func(ctx context.Context, msg *schema.AgenticMessage) (*schema.AgenticMessage, error) {
_, err := agenticAgent.applyAfterAgent(ctx)
return msg, err
}
}
return func(ctx context.Context, p *typedRunParams[M]) {
ap := any(p).(*typedRunParams[*schema.AgenticMessage])
cancelCtx := ap.cancelCtx
agenticConf.cancelCtx = cancelCtx
if agenticConf.modelWrapperConf != nil {
agenticConf.modelWrapperConf.cancelContext = cancelCtx
}
ctx = withCancelContext(ctx, cancelCtx)
g, err := newAgenticReact(ctx, agenticConf)
if err != nil {
ap.generator.Send(&TypedAgentEvent[*schema.AgenticMessage]{Err: err})
return
}
chain := compose.NewChain[agenticReactRunInput, *schema.AgenticMessage]().
AppendLambda(
compose.InvokableLambda(func(ctx context.Context, in agenticReactRunInput) (*agenticReactInput, error) {
messages, genErr := genModelInputFn(ctx, in.instruction, in.input)
if genErr != nil {
return nil, genErr
}
return &agenticReactInput{
Messages: messages,
}, nil
}),
).
AppendGraph(g, compose.WithNodeName("ReAct"), compose.WithGraphCompileOptions(compose.WithMaxRunSteps(math.MaxInt)))
var compileOptions []compose.GraphCompileOption
compileOptions = append(compileOptions,
compose.WithGraphName(a.name),
compose.WithCheckPointStore(ap.store),
compose.WithSerializer(&gobSerializer{}),
compose.WithMaxRunSteps(math.MaxInt))
if cancelCtx != nil {
var interrupt func(...compose.GraphInterruptOption)
ctx, interrupt = compose.WithGraphInterrupt(ctx)
cancelCtx.setGraphInterruptFunc(interrupt)
}
runnable, err_ := chain.Compile(ctx, compileOptions...)
if err_ != nil {
ap.generator.Send(&TypedAgentEvent[*schema.AgenticMessage]{Err: err_})
return
}
ctx = withTypedChatModelAgentExecCtx(ctx, &typedChatModelAgentExecCtx[*schema.AgenticMessage]{
runtimeReturnDirectly: ap.returnDirectly,
generator: ap.generator,
cancelCtx: cancelCtx,
failoverLastSuccessModel: agenticModel,
afterToolCallsHook: ap.afterToolCallsHook,
})
// Pre-execution cancel check
if cancelCtx != nil && cancelCtx.shouldCancel() {
if cancelCtx.getMode() == CancelImmediate || atomic.LoadInt32(&cancelCtx.escalated) == 1 {
cancelErr, ok := cancelCtx.createAndMarkCancelHandled()
if !ok {
return
}
ap.generator.Send(&TypedAgentEvent[*schema.AgenticMessage]{Err: cancelErr})
return
}
}
in := agenticReactRunInput{input: ap.input, instruction: ap.instruction}
var runOpts []compose.Option
runOpts = append(runOpts, ap.composeOpts...)
if a.toolsConfig.EmitInternalEvents {
runOpts = append(runOpts, compose.WithToolsNodeOption(compose.WithToolOption(withTypedAgentToolEventGenerator[*schema.AgenticMessage](ap.generator))))
}
if ap.input.EnableStreaming {
runOpts = append(runOpts, compose.WithToolsNodeOption(compose.WithToolOption(withAgentToolEnableStreaming(true))))
}
var msg *schema.AgenticMessage
var msgStream *schema.StreamReader[*schema.AgenticMessage]
if ap.input.EnableStreaming {
msgStream, err_ = runnable.Stream(ctx, in, runOpts...)
} else {
msg, err_ = runnable.Invoke(ctx, in, runOpts...)
}
if err_ == nil {
if a.outputKey != "" {
err_ = setOutputToSession(ctx, msg, msgStream, a.outputKey)
if err_ != nil {
ap.generator.Send(&TypedAgentEvent[*schema.AgenticMessage]{Err: err_})
}
} else if msgStream != nil {
msgStream.Close()
}
return
}
a.handleRunFuncError(ctx, err_, cancelCtx, ap.cancelCtxOwned, ap.store, p.generator)
}, nil
}
func (a *TypedChatModelAgent[M]) buildRunFunc(ctx context.Context) typedRunFunc[M] {
a.once.Do(func() {
ec, err := a.prepareExecContext(ctx)
if err != nil {
a.run = typedErrFunc[M](err)
return
}
a.exeCtx = ec
if len(ec.toolsNodeConf.Tools) == 0 {
var run typedRunFunc[M]
run, err = a.buildNoToolsRunFunc(ctx)
if err != nil {
a.run = typedErrFunc[M](err)
return
}
a.run = run
return
}
var run typedRunFunc[M]
run, err = a.buildReActRunFunc(ctx, ec)
if err != nil {
a.run = typedErrFunc[M](err)
return
}
a.run = run
})
atomic.StoreUint32(&a.frozen, 1)
return a.run
}
func (a *TypedChatModelAgent[M]) getRunFunc(ctx context.Context) (context.Context, typedRunFunc[M], *execContext, error) {
defaultRun := a.buildRunFunc(ctx)
bc := a.exeCtx
if bc == nil {
return ctx, defaultRun, bc, nil
}
if len(a.handlers) == 0 {
runtimeBC := &execContext{
instruction: bc.instruction,
toolsNodeConf: bc.toolsNodeConf,
returnDirectly: bc.returnDirectly,
toolInfos: bc.toolInfos,
}
return ctx, defaultRun, runtimeBC, nil
}
ctx, runtimeBC, err := a.applyBeforeAgent(ctx, bc)
if err != nil {
return ctx, nil, nil, err
}
if !runtimeBC.rebuildGraph {
return ctx, defaultRun, runtimeBC, nil
}
var tempRun typedRunFunc[M]
if len(runtimeBC.toolsNodeConf.Tools) == 0 {
tempRun, err = a.buildNoToolsRunFunc(ctx)
if err != nil {
return ctx, nil, nil, err
}
} else {
tempRun, err = a.buildReActRunFunc(ctx, runtimeBC)
if err != nil {
return ctx, nil, nil, err
}
}
return ctx, tempRun, runtimeBC, nil
}
func (a *TypedChatModelAgent[M]) Run(ctx context.Context, input *TypedAgentInput[M], opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] {
iterator, generator := NewAsyncIteratorPair[*TypedAgentEvent[M]]()
o := getCommonOptions(nil, opts...)
cancelCtx, cancelCtxOwned := resolveRunCancelContext(ctx, o)
ctx, run, bc, err := a.getRunFunc(ctx)
if err != nil {
go func() {
if cancelCtxOwned && cancelCtx != nil {
defer cancelCtx.markDone()
}
generator.Send(&TypedAgentEvent[M]{Err: fmt.Errorf("ChatModelAgent getRunFunc error: %w", err)})
generator.Close()
}()
return iterator
}
co := getComposeOptions(opts)
co = append(co, compose.WithCheckPointID(bridgeCheckpointID))
runOps := GetImplSpecificOptions[chatModelAgentRunOptions](nil, opts...)
if bc != nil {
if len(bc.toolInfos) > 0 {
co = append(co, compose.WithChatModelOption(model.WithTools(bc.toolInfos)))
}
if bc.toolSearchTool != nil {
co = append(co, compose.WithChatModelOption(model.WithToolSearchTool(bc.toolSearchTool)))
}
if bc.toolUpdated {
co = append(co, compose.WithToolsNodeOption(compose.WithToolList(bc.toolsNodeConf.Tools...)))
}
}
go func() {
defer func() {
panicErr := recover()
if panicErr != nil {
e := safe.NewPanicErr(panicErr, debug.Stack())
generator.Send(&TypedAgentEvent[M]{Err: e})
}
generator.Close()
}()
var (
instruction string
returnDirectly map[string]bool
)
if bc != nil {
instruction = bc.instruction
returnDirectly = bc.returnDirectly
}
run(ctx, &typedRunParams[M]{
input: input,
generator: generator,
store: newBridgeStore(),
instruction: instruction,
returnDirectly: returnDirectly,
cancelCtx: cancelCtx,
cancelCtxOwned: cancelCtxOwned,
composeOpts: co,
afterToolCallsHook: runOps.afterToolCallsHook,
})
}()
if cancelCtxOwned {
return wrapIterWithCancelCtx(iterator, cancelCtx)
}
return iterator
}
func (a *TypedChatModelAgent[M]) Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] {
iterator, generator := NewAsyncIteratorPair[*TypedAgentEvent[M]]()
o := getCommonOptions(nil, opts...)
cancelCtx, cancelCtxOwned := resolveRunCancelContext(ctx, o)
ctx, run, bc, err := a.getRunFunc(ctx)
if err != nil {
go func() {
if cancelCtxOwned && cancelCtx != nil {
defer cancelCtx.markDone()
}
generator.Send(&TypedAgentEvent[M]{Err: fmt.Errorf("ChatModelAgent getRunFunc error: %w", err)})
generator.Close()
}()
return iterator
}
co := getComposeOptions(opts)
co = append(co, compose.WithCheckPointID(bridgeCheckpointID))
resumeRunOps := GetImplSpecificOptions[chatModelAgentRunOptions](nil, opts...)
if bc != nil {
if len(bc.toolInfos) > 0 {
co = append(co, compose.WithChatModelOption(model.WithTools(bc.toolInfos)))
}
if bc.toolSearchTool != nil {
co = append(co, compose.WithChatModelOption(model.WithToolSearchTool(bc.toolSearchTool)))
}
if bc.toolUpdated {
co = append(co, compose.WithToolsNodeOption(compose.WithToolList(bc.toolsNodeConf.Tools...)))
}
}
if info == nil {
panic(fmt.Sprintf("ChatModelAgent.Resume: agent '%s' was asked to resume but info is nil", a.Name(ctx)))
}
if info.InterruptState == nil {
panic(fmt.Sprintf("ChatModelAgent.Resume: agent '%s' was asked to resume but has no state", a.Name(ctx)))
}
stateByte, ok := info.InterruptState.([]byte)
if !ok {
panic(fmt.Sprintf("ChatModelAgent.Resume: agent '%s' was asked to resume but has invalid interrupt state type: %T",
a.Name(ctx), info.InterruptState))
}
// Migrate legacy checkpoints before resume.
// This covers both:
// - v0.7.*: state is stored as a struct wire type (stateV07) under the legacy name.
// - v0.8.0-v0.8.3: state is stored as a GobEncoder payload under the same legacy name and must
// be routed to a GobDecode-compatible compat type via byte-patching.
// The result is re-encoded so the resume path always operates on the current *State.
stateByte, err = preprocessComposeCheckpoint(stateByte)
if err != nil {
go func() {
generator.Send(&TypedAgentEvent[M]{Err: err})
generator.Close()
}()
return iterator
}
var historyModifier func(ctx context.Context, history []Message) []Message
if info.ResumeData != nil {
resumeData, ok := info.ResumeData.(*ChatModelAgentResumeData)
if !ok {
panic(fmt.Sprintf("ChatModelAgent.Resume: agent '%s' was asked to resume but has invalid resume data type: %T",
a.Name(ctx), info.ResumeData))
}
historyModifier = resumeData.HistoryModifier
}
if historyModifier != nil {
co = append(co, compose.WithStateModifier(func(ctx context.Context, path compose.NodePath, state any) error {
s, ok := state.(*State)
if !ok {
return nil
}
s.Messages = historyModifier(ctx, s.Messages)
return nil
}))
}
go func() {
defer func() {
panicErr := recover()
if panicErr != nil {
e := safe.NewPanicErr(panicErr, debug.Stack())
generator.Send(&TypedAgentEvent[M]{Err: e})
}
generator.Close()
}()
var (
instruction string
returnDirectly map[string]bool
)
if bc != nil {
instruction = bc.instruction
returnDirectly = bc.returnDirectly
}
run(ctx, &typedRunParams[M]{
input: &TypedAgentInput[M]{EnableStreaming: info.EnableStreaming},
generator: generator,
store: newResumeBridgeStore(bridgeCheckpointID, stateByte),
instruction: instruction,
returnDirectly: returnDirectly,
cancelCtx: cancelCtx,
cancelCtxOwned: cancelCtxOwned,
composeOpts: co,
afterToolCallsHook: resumeRunOps.afterToolCallsHook,
})
}()
if cancelCtxOwned {
return wrapIterWithCancelCtx(iterator, cancelCtx)
}
return iterator
}
func getComposeOptions(opts []AgentRunOption) []compose.Option {
o := GetImplSpecificOptions[chatModelAgentRunOptions](nil, opts...)
var co []compose.Option
if len(o.chatModelOptions) > 0 {
co = append(co, compose.WithChatModelOption(o.chatModelOptions...))
}
var to []tool.Option
if len(o.toolOptions) > 0 {
to = append(to, o.toolOptions...)
}
for toolName, atos := range o.agentToolOptions {
to = append(to, withAgentToolOptions(toolName, atos))
}
if len(to) > 0 {
co = append(co, compose.WithToolsNodeOption(compose.WithToolOption(to...)))
}
if o.historyModifier != nil {
co = append(co, compose.WithStateModifier(func(ctx context.Context, path compose.NodePath, state any) error {
s, ok := state.(*State)
if !ok {
return fmt.Errorf("unexpected state type: %T, expected: %T", state, &State{})
}
s.Messages = o.historyModifier(ctx, s.Messages)
return nil
}))
}
return co
}
type gobSerializer struct{}
func (g *gobSerializer) Marshal(v any) ([]byte, error) {
buf := new(bytes.Buffer)
err := gob.NewEncoder(buf).Encode(v)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (g *gobSerializer) Unmarshal(data []byte, v any) error {
buf := bytes.NewBuffer(data)
return gob.NewDecoder(buf).Decode(v)
}
// preprocessComposeCheckpoint migrates legacy compose checkpoints to the current format.
// It handles the v0.8.0-v0.8.3 format:
// - gob name "_eino_adk_state_v080_" (already byte-patched by preprocessADKCheckpoint
// from "_eino_adk_react_state"), opaque-bytes wire format → decoded as *stateV080
//
// v0.7 checkpoints need no migration — State is now a plain struct registered under the
// same gob name, and gob handles missing fields gracefully.
//
// Fast path: if the legacy name is not present, skip entirely.
func preprocessComposeCheckpoint(data []byte) ([]byte, error) {
const lenPrefixedCompatName = "\x15" + stateGobNameV080
if bytes.Contains(data, []byte(lenPrefixedCompatName)) {
// v0.8.0-v0.8.3: already byte-patched by preprocessADKCheckpoint; decode as *stateV080.
migrated, err := compose.MigrateCheckpointState(data, &gobSerializer{}, func(state any) (any, bool, error) {
sc, ok := state.(*stateV080)
if !ok {
return state, false, nil
}
return stateV080ToState(sc), true, nil
})
if err != nil {
return nil, fmt.Errorf("failed to migrate v0.8.0-v0.8.3 compose checkpoint: %w", err)
}
return migrated, nil
}
return data, nil
}