/*
 * 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
}
