/*
 * 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 (
	"context"
	"errors"
	"fmt"
	"log"
	"runtime/debug"
	"strings"

	"github.com/cloudwego/eino/callbacks"
	"github.com/cloudwego/eino/compose"
	icb "github.com/cloudwego/eino/internal/callbacks"
	"github.com/cloudwego/eino/internal/safe"
	"github.com/cloudwego/eino/schema"
)

type HistoryEntry struct {
	IsUserInput bool
	AgentName   string
	Message     Message
}

type HistoryRewriter func(ctx context.Context, entries []*HistoryEntry) ([]Message, error)

type flowAgent struct {
	Agent

	subAgents   []*flowAgent
	parentAgent *flowAgent

	disallowTransferToParent bool
	historyRewriter          HistoryRewriter

	checkPointStore compose.CheckPointStore
}

func (a *flowAgent) deepCopy() *flowAgent {
	ret := &flowAgent{
		Agent:                    a.Agent,
		subAgents:                make([]*flowAgent, 0, len(a.subAgents)),
		parentAgent:              a.parentAgent,
		disallowTransferToParent: a.disallowTransferToParent,
		historyRewriter:          a.historyRewriter,
		checkPointStore:          a.checkPointStore,
	}

	for _, sa := range a.subAgents {
		ret.subAgents = append(ret.subAgents, sa.deepCopy())
	}
	return ret
}

// SetSubAgents sets sub-agents for the given agent and returns the updated agent.
//
// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven
// to be more effective empirically. Consider using ChatModelAgent with AgentTool
// or DeepAgent instead for most multi-agent scenarios.
func SetSubAgents(ctx context.Context, agent Agent, subAgents []Agent) (ResumableAgent, error) {
	return setSubAgents(ctx, agent, subAgents)
}

type AgentOption func(options *flowAgent)

// WithDisallowTransferToParent prevents a sub-agent from transferring to its parent.
//
// 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 WithDisallowTransferToParent() AgentOption {
	return func(fa *flowAgent) {
		fa.disallowTransferToParent = true
	}
}

// WithHistoryRewriter sets a rewriter to transform conversation history
// during agent transfers.
//
// 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 WithHistoryRewriter(h HistoryRewriter) AgentOption {
	return func(fa *flowAgent) {
		fa.historyRewriter = h
	}
}

func toFlowAgent(ctx context.Context, agent Agent, opts ...AgentOption) *flowAgent {
	var fa *flowAgent
	var ok bool
	if fa, ok = agent.(*flowAgent); !ok {
		fa = &flowAgent{Agent: agent}
	} else {
		fa = fa.deepCopy()
	}
	for _, opt := range opts {
		opt(fa)
	}

	if fa.historyRewriter == nil {
		fa.historyRewriter = buildDefaultHistoryRewriter(agent.Name(ctx))
	}

	return fa
}

// AgentWithOptions wraps an agent with flow-specific options and returns it.
//
// 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 AgentWithOptions(ctx context.Context, agent Agent, opts ...AgentOption) Agent {
	return toFlowAgent(ctx, agent, opts...)
}

func setSubAgents(ctx context.Context, agent Agent, subAgents []Agent) (*flowAgent, error) {
	fa := toFlowAgent(ctx, agent)

	if len(fa.subAgents) > 0 {
		return nil, errors.New("agent's sub-agents has already been set")
	}

	if onAgent, ok_ := fa.Agent.(OnSubAgents); ok_ {
		err := onAgent.OnSetSubAgents(ctx, subAgents)
		if err != nil {
			return nil, err
		}
	}

	for _, s := range subAgents {
		fsa := toFlowAgent(ctx, s)

		if fsa.parentAgent != nil {
			return nil, errors.New("agent has already been set as a sub-agent of another agent")
		}

		fsa.parentAgent = fa
		if onAgent, ok__ := fsa.Agent.(OnSubAgents); ok__ {
			err := onAgent.OnSetAsSubAgent(ctx, agent)
			if err != nil {
				return nil, err
			}

			if fsa.disallowTransferToParent {
				err = onAgent.OnDisallowTransferToParent(ctx)
				if err != nil {
					return nil, err
				}
			}
		}

		fa.subAgents = append(fa.subAgents, fsa)
	}

	return fa, nil
}

func (a *flowAgent) getAgent(ctx context.Context, name string) *flowAgent {
	for _, subAgent := range a.subAgents {
		if subAgent.Name(ctx) == name {
			return subAgent
		}
	}

	if a.parentAgent != nil && a.parentAgent.Name(ctx) == name {
		return a.parentAgent
	}

	return nil
}

func rewriteMessage(msg Message, agentName string) Message {
	var sb strings.Builder
	sb.WriteString("For context:")
	if msg.Role == schema.Assistant {
		if msg.Content != "" {
			sb.WriteString(fmt.Sprintf(" [%s] said: %s.", agentName, msg.Content))
		}
		if len(msg.ToolCalls) > 0 {
			for i := range msg.ToolCalls {
				f := msg.ToolCalls[i].Function
				sb.WriteString(fmt.Sprintf(" [%s] called tool: `%s` with arguments: %s.",
					agentName, f.Name, f.Arguments))
			}
		}
	} else if msg.Role == schema.Tool && msg.Content != "" {
		sb.WriteString(fmt.Sprintf(" [%s] `%s` tool returned result: %s.",
			agentName, msg.ToolName, msg.Content))
	}

	rewritten := schema.UserMessage(sb.String())
	if msg.MultiContent != nil {
		rewritten.MultiContent = append([]schema.ChatMessagePart{}, msg.MultiContent...) //nolint:staticcheck // backward compat with deprecated MultiContent field
	}
	if msg.UserInputMultiContent != nil {
		rewritten.UserInputMultiContent = append([]schema.MessageInputPart{}, msg.UserInputMultiContent...)
	}

	// Convert AssistantGenMultiContent to UserInputMultiContent, since the role changes to User.
	// Reasoning parts have no user input equivalent and are dropped.
	for _, part := range msg.AssistantGenMultiContent {
		switch part.Type {
		case schema.ChatMessagePartTypeText:
			rewritten.UserInputMultiContent = append(rewritten.UserInputMultiContent, schema.MessageInputPart{
				Type:  part.Type,
				Text:  part.Text,
				Extra: part.Extra,
			})
		case schema.ChatMessagePartTypeImageURL:
			if part.Image != nil {
				rewritten.UserInputMultiContent = append(rewritten.UserInputMultiContent, schema.MessageInputPart{
					Type:  part.Type,
					Image: &schema.MessageInputImage{MessagePartCommon: part.Image.MessagePartCommon},
					Extra: part.Extra,
				})
			}
		case schema.ChatMessagePartTypeAudioURL:
			if part.Audio != nil {
				rewritten.UserInputMultiContent = append(rewritten.UserInputMultiContent, schema.MessageInputPart{
					Type:  part.Type,
					Audio: &schema.MessageInputAudio{MessagePartCommon: part.Audio.MessagePartCommon},
					Extra: part.Extra,
				})
			}
		case schema.ChatMessagePartTypeVideoURL:
			if part.Video != nil {
				rewritten.UserInputMultiContent = append(rewritten.UserInputMultiContent, schema.MessageInputPart{
					Type:  part.Type,
					Video: &schema.MessageInputVideo{MessagePartCommon: part.Video.MessagePartCommon},
					Extra: part.Extra,
				})
			}
		}
	}

	return rewritten
}

func genMsg(entry *HistoryEntry, agentName string) (Message, error) {
	msg := entry.Message
	if entry.AgentName != agentName {
		msg = rewriteMessage(msg, entry.AgentName)
	}

	return msg, nil
}

func deepCopyAgentInput(ai *AgentInput) *AgentInput {
	copied := &AgentInput{
		Messages:        make([]Message, len(ai.Messages)),
		EnableStreaming: ai.EnableStreaming,
	}

	copy(copied.Messages, ai.Messages)

	return copied
}

func (a *flowAgent) genAgentInput(ctx context.Context, runCtx *runContext, skipTransferMessages bool) (*AgentInput, error) {
	input := deepCopyAgentInput(runCtx.RootInput)

	events := runCtx.Session.getEvents()
	historyEntries := make([]*HistoryEntry, 0)

	for _, m := range input.Messages {
		historyEntries = append(historyEntries, &HistoryEntry{
			IsUserInput: true,
			Message:     m,
		})
	}

	for _, event := range events {
		if skipTransferMessages && event.Action != nil && event.Action.TransferToAgent != nil {
			// If skipTransferMessages is true and the event contain transfer action, the message in this event won't be appended to history entries.
			if event.Output != nil &&
				event.Output.MessageOutput != nil &&
				event.Output.MessageOutput.Role == schema.Tool &&
				len(historyEntries) > 0 {
				// If the skipped message's role is Tool, remove the previous history entry as it's also a transfer message(from ChatModelAgent and GenTransferMessages).
				historyEntries = historyEntries[:len(historyEntries)-1]
			}
			continue
		}

		msg, err := getMessageFromWrappedEvent(event)
		if err != nil {
			var retryErr *WillRetryError
			if errors.As(err, &retryErr) {
				log.Printf("failed to get message from event, but will retry: %v", err)
				continue
			}
			return nil, err
		}

		if msg == nil {
			continue
		}

		historyEntries = append(historyEntries, &HistoryEntry{
			AgentName: event.AgentName,
			Message:   msg,
		})
	}

	messages, err := a.historyRewriter(ctx, historyEntries)
	if err != nil {
		return nil, err
	}
	input.Messages = messages

	return input, nil
}

func buildDefaultHistoryRewriter(agentName string) HistoryRewriter {
	return func(ctx context.Context, entries []*HistoryEntry) ([]Message, error) {
		messages := make([]Message, 0, len(entries))
		var err error
		for _, entry := range entries {
			msg := entry.Message
			if !entry.IsUserInput {
				msg, err = genMsg(entry, agentName)
				if err != nil {
					return nil, fmt.Errorf("gen agent input failed: %w", err)
				}
			}

			if msg != nil {
				messages = append(messages, msg)
			}
		}

		return messages, nil
	}
}

func (a *flowAgent) Run(ctx context.Context, input *AgentInput, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] {
	agentName := a.Name(ctx)

	var runCtx *runContext
	ctx, runCtx = initRunCtx(ctx, agentName, input)
	ctx = AppendAddressSegment(ctx, AddressSegmentAgent, agentName)

	o := getCommonOptions(nil, opts...)
	cancelCtx := o.cancelCtx

	processedInput, err := a.genAgentInput(ctx, runCtx, o.skipTransferMessages)
	if err != nil {
		if cancelCtx != nil {
			cancelCtx.markDone()
		}
		cbInput := &AgentCallbackInput{Input: input}
		ctx = callbacks.OnStart(ctx, cbInput)
		return wrapIterWithOnEnd(ctx, genErrorIter(err))
	}

	ctxForSubAgents := ctx

	agentType := getAgentType(a.Agent)
	ctx = initAgentCallbacks(ctx, agentName, agentType, filterOptions(agentName, opts)...)
	cbInput := &AgentCallbackInput{Input: processedInput}
	ctx = callbacks.OnStart(ctx, cbInput)

	input = processedInput

	if wf, ok := a.Agent.(*workflowAgent); ok {
		ctx = withCancelContext(ctx, cancelCtx)
		filteredOpts := filterCancelOption(filterCallbackHandlersForNestedAgents(agentName, opts))
		iter := wf.Run(ctx, input, filteredOpts...)
		iter = wrapIterWithCancelCtx(iter, cancelCtx)
		return wrapIterWithOnEnd(ctx, iter)
	}

	aIter := a.Agent.Run(withCancelContext(ctx, cancelCtx), input, filterOptions(agentName, opts)...)

	iterator, generator := NewAsyncIteratorPair[*AgentEvent]()

	go a.run(withCancelContext(ctx, cancelCtx), withCancelContext(ctxForSubAgents, cancelCtx), runCtx, aIter, generator, filterCancelOption(opts)...)

	return wrapIterWithCancelCtx(iterator, cancelCtx)
}

func (a *flowAgent) Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] {
	agentName := a.Name(ctx)

	ctx, info = buildResumeInfo(ctx, agentName, info)

	ctxForSubAgents := ctx

	o := getCommonOptions(nil, opts...)
	cancelCtx := o.cancelCtx

	agentType := getAgentType(a.Agent)
	ctx = initAgentCallbacks(ctx, agentName, agentType, filterOptions(agentName, opts)...)
	cbInput := &AgentCallbackInput{ResumeInfo: info}
	ctx = callbacks.OnStart(ctx, cbInput)

	if info.WasInterrupted {
		if ra, ok := a.Agent.(ResumableAgent); ok {
			if _, ok := ra.(*workflowAgent); ok {
				ctx = withCancelContext(ctx, cancelCtx)
				filteredOpts := filterCancelOption(filterCallbackHandlersForNestedAgents(agentName, opts))
				aIter := ra.Resume(ctx, info, filteredOpts...)
				aIter = wrapIterWithCancelCtx(aIter, cancelCtx)
				return wrapIterWithOnEnd(ctx, aIter)
			}

			aIter := ra.Resume(withCancelContext(ctx, cancelCtx), info, opts...)

			iterator, generator := NewAsyncIteratorPair[*AgentEvent]()
			go a.run(withCancelContext(ctx, cancelCtx), withCancelContext(ctxForSubAgents, cancelCtx), getRunCtx(ctxForSubAgents), aIter, generator, filterCancelOption(opts)...)
			return wrapIterWithCancelCtx(iterator, cancelCtx)
		}

		if cancelCtx != nil {
			cancelCtx.markDone()
		}
		return wrapIterWithOnEnd(ctx, genErrorIter(fmt.Errorf("failed to resume agent: agent '%s' is an interrupt point "+
			"but is not a ResumableAgent", agentName)))
	}

	nextAgentName, err := getNextResumeAgent(ctx, info)
	if err != nil {
		if cancelCtx != nil {
			cancelCtx.markDone()
		}
		return wrapIterWithOnEnd(ctx, genErrorIter(err))
	}

	subAgent := a.getAgent(ctxForSubAgents, nextAgentName)
	if subAgent == nil {
		if len(a.subAgents) == 0 {
			if ra, ok := a.Agent.(ResumableAgent); ok {
				ctx = withCancelContext(ctx, cancelCtx)
				innerIter := ra.Resume(ctx, info, filterCancelOption(opts)...)
				return wrapIterWithCancelCtx(wrapIterWithOnEnd(ctx, innerIter), cancelCtx)
			}
			return wrapIterWithOnEnd(ctx, genErrorIter(fmt.Errorf(
				"failed to resume agent: agent '%s' (type %T) has no sub-agents and does not implement ResumableAgent interface. "+
					"To support resume, your custom agent wrapper must implement the ResumableAgent interface", agentName, a.Agent)))
		}
		if cancelCtx != nil {
			cancelCtx.markDone()
		}
		return wrapIterWithOnEnd(ctx, genErrorIter(fmt.Errorf("failed to resume agent: sub-agent '%s' not found in agent '%s'", nextAgentName, agentName)))
	}

	ctxForSubAgents = withCancelContext(ctxForSubAgents, cancelCtx)
	innerIter := subAgent.Resume(ctxForSubAgents, info, filterCancelOption(opts)...)
	return wrapIterWithCancelCtx(wrapIterWithOnEnd(ctx, innerIter), cancelCtx)
}

// DeterministicTransferConfig is the configuration for AgentWithDeterministicTransferTo.
//
// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven
// to be more effective empirically. Consider using ChatModelAgent with AgentTool
// or DeepAgent instead for most multi-agent scenarios.
type DeterministicTransferConfig struct {
	Agent        Agent
	ToAgentNames []string
}

func (a *flowAgent) run(
	ctx context.Context,
	ctxForSubAgents context.Context,
	runCtx *runContext,
	aIter *AsyncIterator[*AgentEvent],
	generator *AsyncGenerator[*AgentEvent],
	opts ...AgentRunOption) {

	cbIter, cbGen := NewAsyncIteratorPair[*AgentEvent]()

	cbOutput := &AgentCallbackOutput{Events: cbIter}
	icb.On(ctx, cbOutput, icb.BuildOnEndHandleWithCopy(copyAgentCallbackOutput), callbacks.TimingOnEnd, false)

	defer func() {
		panicErr := recover()
		if panicErr != nil {
			e := safe.NewPanicErr(panicErr, debug.Stack())
			generator.Send(&AgentEvent{Err: e})
		}

		cbGen.Close()
		generator.Close()
	}()

	var lastAction *AgentAction
	for {
		event, ok := aIter.Next()
		if !ok {
			break
		}

		// RunPath ownership: the eino framework sets RunPath exactly once.
		// If event.RunPath is already set (e.g., by agentTool), we don't modify it.
		// If event.RunPath is nil/empty, we set it to the current runCtx.RunPath.
		// This ensures RunPath is set exactly once and not duplicated.
		if len(event.RunPath) == 0 {
			event.AgentName = a.Name(ctx)
			event.RunPath = runCtx.RunPath
		}
		// Recording policy: exact RunPath match (non-interrupt) indicates events belonging to this agent execution.
		// This prevents parent recording of child/tool-internal emissions.
		if (event.Action == nil || event.Action.Interrupted == nil) && exactRunPathMatch(runCtx.RunPath, event.RunPath) {
			// copy the event so that the copied event's stream is exclusive for any potential consumer
			// copy before adding to session because once added to session it's stream could be consumed by genAgentInput at any time
			// interrupt action are not added to session, because ALL information contained in it
			// is either presented to end-user, or made available to agents through other means
			copied := copyTypedAgentEvent(event)
			setAutomaticClose(copied)
			setAutomaticClose(event)
			runCtx.Session.addEvent(copied)
		}
		// Action gating uses exact run-path match as well:
		// only actions originating from this agent execution (not child/tool runs)
		// should influence parent control flow (exit/transfer/interrupt).
		if exactRunPathMatch(runCtx.RunPath, event.RunPath) {
			lastAction = event.Action
		}
		copied := copyTypedAgentEvent(event)
		setAutomaticClose(copied)
		setAutomaticClose(event)
		cbGen.Send(copied)
		generator.Send(event)
	}

	var destName string
	if lastAction != nil {
		if lastAction.Interrupted != nil {
			return
		}
		if lastAction.Exit {
			return
		}

		if lastAction.TransferToAgent != nil {
			destName = lastAction.TransferToAgent.DestAgentName
		}
	}

	// handle transferring to another agent
	if destName != "" {
		agentToRun := a.getAgent(ctxForSubAgents, destName)
		if agentToRun == nil {
			e := fmt.Errorf("transfer failed: agent '%s' not found when transferring from '%s'",
				destName, a.Name(ctxForSubAgents))
			generator.Send(&AgentEvent{Err: e})
			return
		}

		subAIter := agentToRun.Run(ctxForSubAgents, nil /*subagents get input from runCtx*/, opts...)
		for {
			subEvent, ok_ := subAIter.Next()
			if !ok_ {
				break
			}

			setAutomaticClose(subEvent)
			generator.Send(subEvent)
		}
	}
}

func exactRunPathMatch(aPath, bPath []RunStep) bool {
	if len(aPath) != len(bPath) {
		return false
	}
	for i := range aPath {
		if !aPath[i].Equals(bPath[i]) {
			return false
		}
	}
	return true
}

func wrapIterWithOnEnd(ctx context.Context, iter *AsyncIterator[*AgentEvent]) *AsyncIterator[*AgentEvent] {
	cbIter, cbGen := NewAsyncIteratorPair[*AgentEvent]()
	cbOutput := &AgentCallbackOutput{Events: cbIter}
	icb.On(ctx, cbOutput, icb.BuildOnEndHandleWithCopy(copyAgentCallbackOutput), callbacks.TimingOnEnd, false)

	outIter, outGen := NewAsyncIteratorPair[*AgentEvent]()
	go func() {
		defer func() {
			cbGen.Close()
			outGen.Close()
		}()
		for {
			event, ok := iter.Next()
			if !ok {
				break
			}
			copied := copyTypedAgentEvent(event)
			cbGen.Send(copied)
			outGen.Send(event)
		}
	}()
	return outIter
}

// ---------------------------------------------------------------------------
// Typed wrapper for the agentic path (TypedAgent[*schema.AgenticMessage]).
//
// typedFlowAgent is a minimal wrapper used exclusively by TypedRunner and
// AgentTool to execute a TypedAgent[*schema.AgenticMessage]. It handles
// callbacks, event recording, and run-path tracking. Transfer, sub-agent
// orchestration, and history rewriting are handled solely by the concrete
// flowAgent (the *schema.Message path).
// ---------------------------------------------------------------------------

type typedFlowAgent[M MessageType] struct {
	TypedAgent[M]

	checkPointStore compose.CheckPointStore
}

func toTypedFlowAgent[M MessageType](agent TypedAgent[M]) *typedFlowAgent[M] {
	if fa, ok := agent.(*typedFlowAgent[M]); ok {
		return fa
	}
	return &typedFlowAgent[M]{TypedAgent: agent}
}

func getTypedAgentType[M MessageType](agent TypedAgent[M]) string {
	if msgAgent, ok := any(agent).(Agent); ok {
		return getAgentType(msgAgent)
	}
	if typer, ok := any(agent).(interface{ GetType() string }); ok {
		return typer.GetType()
	}
	return ""
}

func (a *typedFlowAgent[M]) Run(ctx context.Context, input *TypedAgentInput[M], opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] {
	agentName := a.Name(ctx)

	var runCtx *runContext
	ctx, runCtx = initTypedRunCtx(ctx, agentName, input)
	ctx = AppendAddressSegment(ctx, AddressSegmentAgent, agentName)

	o := getCommonOptions(nil, opts...)
	cancelCtx := o.cancelCtx

	ctxForSubAgents := ctx

	agentType := getTypedAgentType(a.TypedAgent)
	ctx = initAgenticCallbacks(ctx, agentName, agentType, filterOptions(agentName, opts)...)
	cbInput := &TypedAgentCallbackInput[*schema.AgenticMessage]{Input: any(input).(*TypedAgentInput[*schema.AgenticMessage])}
	ctx = callbacks.OnStart(ctx, cbInput)

	aIter := a.TypedAgent.Run(withCancelContext(ctx, cancelCtx), input, filterOptions(agentName, opts)...)

	iterator, generator := NewAsyncIteratorPair[*TypedAgentEvent[M]]()

	go a.run(withCancelContext(ctx, cancelCtx), withCancelContext(ctxForSubAgents, cancelCtx), runCtx, aIter, generator, filterCancelOption(opts)...)

	return wrapIterWithCancelCtx(iterator, cancelCtx)
}

func (a *typedFlowAgent[M]) Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] {
	agentName := a.Name(ctx)

	ctx, info = buildResumeInfo(ctx, agentName, info)

	ctxForSubAgents := ctx

	o := getCommonOptions(nil, opts...)
	cancelCtx := o.cancelCtx

	agentType := getTypedAgentType(a.TypedAgent)
	ctx = initAgenticCallbacks(ctx, agentName, agentType, filterOptions(agentName, opts)...)
	cbInput := &TypedAgentCallbackInput[*schema.AgenticMessage]{ResumeInfo: info}
	ctx = callbacks.OnStart(ctx, cbInput)

	if info.WasInterrupted {
		if ra, ok := a.TypedAgent.(TypedResumableAgent[M]); ok {
			aIter := ra.Resume(withCancelContext(ctx, cancelCtx), info, opts...)

			iterator, generator := NewAsyncIteratorPair[*TypedAgentEvent[M]]()
			go a.run(withCancelContext(ctx, cancelCtx), withCancelContext(ctxForSubAgents, cancelCtx), getRunCtx(ctxForSubAgents), aIter, generator, filterCancelOption(opts)...)
			return wrapIterWithCancelCtx(iterator, cancelCtx)
		}

		if cancelCtx != nil {
			cancelCtx.markDone()
		}
		return typedErrorIterWithOnEnd[M](ctx, fmt.Errorf("failed to resume agent: agent '%s' is an interrupt point "+
			"but is not a ResumableAgent", agentName))
	}

	_, err := getNextResumeAgent(ctx, info)
	if err != nil {
		if cancelCtx != nil {
			cancelCtx.markDone()
		}
		return typedErrorIterWithOnEnd[M](ctx, err)
	}

	if ra, ok := a.TypedAgent.(TypedResumableAgent[M]); ok {
		ctx = withCancelContext(ctx, cancelCtx)
		innerIter := ra.Resume(ctx, info, filterCancelOption(opts)...)
		return wrapIterWithCancelCtx(typedWrapIterWithOnEnd(ctx, innerIter), cancelCtx)
	}
	return typedErrorIterWithOnEnd[M](ctx, fmt.Errorf(
		"failed to resume agent: agent '%s' (type %T) does not implement ResumableAgent interface. "+
			"To support resume, your custom agent wrapper must implement the ResumableAgent interface", agentName, a.TypedAgent))
}

func (a *typedFlowAgent[M]) run(
	ctx context.Context,
	_ context.Context,
	runCtx *runContext,
	aIter *AsyncIterator[*TypedAgentEvent[M]],
	generator *AsyncGenerator[*TypedAgentEvent[M]],
	_ ...AgentRunOption) {

	agenticCbIter, agenticCbGen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]()
	cbOutput := &TypedAgentCallbackOutput[*schema.AgenticMessage]{Events: agenticCbIter}
	icb.On(ctx, cbOutput, icb.BuildOnEndHandleWithCopy(copyTypedCallbackOutput[*schema.AgenticMessage]), callbacks.TimingOnEnd, false)

	defer func() {
		panicErr := recover()
		if panicErr != nil {
			e := safe.NewPanicErr(panicErr, debug.Stack())
			generator.Send(&TypedAgentEvent[M]{Err: e})
		}

		agenticCbGen.Close()
		generator.Close()
	}()

	for {
		event, ok := aIter.Next()
		if !ok {
			break
		}

		if len(event.RunPath) == 0 {
			event.AgentName = a.Name(ctx)
			event.RunPath = runCtx.RunPath
		}
		if (event.Action == nil || event.Action.Interrupted == nil) && exactRunPathMatch(runCtx.RunPath, event.RunPath) {
			copied := copyTypedAgentEvent(event)
			typedSetAutomaticClose(copied)
			typedSetAutomaticClose(event)
			addTypedEvent(runCtx.Session, copied)
		}

		agenticCopied := copyTypedAgentEvent(event)
		typedSetAutomaticClose(agenticCopied)
		typedSetAutomaticClose(event)
		agenticCbGen.Send(any(agenticCopied).(*TypedAgentEvent[*schema.AgenticMessage]))
		generator.Send(event)
	}
}

func wrapAgenticIterWithOnEnd(ctx context.Context, iter *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]]) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] {
	cbIter, cbGen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]()
	cbOutput := &TypedAgentCallbackOutput[*schema.AgenticMessage]{Events: cbIter}
	icb.On(ctx, cbOutput, icb.BuildOnEndHandleWithCopy(copyTypedCallbackOutput[*schema.AgenticMessage]), callbacks.TimingOnEnd, false)

	outIter, outGen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]()
	go func() {
		defer func() {
			cbGen.Close()
			outGen.Close()
		}()
		for {
			event, ok := iter.Next()
			if !ok {
				break
			}
			copied := copyTypedAgentEvent(event)
			cbGen.Send(copied)
			outGen.Send(event)
		}
	}()
	return outIter
}

func genAgenticErrorIter(err error) *AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]] {
	iter, gen := NewAsyncIteratorPair[*TypedAgentEvent[*schema.AgenticMessage]]()
	gen.Send(&TypedAgentEvent[*schema.AgenticMessage]{Err: err})
	gen.Close()
	return iter
}

func typedWrapIterWithOnEnd[M MessageType](ctx context.Context, iter *AsyncIterator[*TypedAgentEvent[M]]) *AsyncIterator[*TypedAgentEvent[M]] {
	agenticIter := any(iter).(*AsyncIterator[*TypedAgentEvent[*schema.AgenticMessage]])
	return any(wrapAgenticIterWithOnEnd(ctx, agenticIter)).(*AsyncIterator[*TypedAgentEvent[M]])
}

func typedErrorIterWithOnEnd[M MessageType](ctx context.Context, err error) *AsyncIterator[*TypedAgentEvent[M]] {
	return typedWrapIterWithOnEnd(ctx, typedErrorIter[M](err))
}
