1715 lines
58 KiB
Go
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
|
|
}
|