package multiagent import ( "context" "errors" "strings" "sync/atomic" "time" "github.com/cloudwego/eino/adk" "github.com/cloudwego/eino/schema" "go.uber.org/zap" ) type einoTurnLoopRuntimeControl interface { Run(context.Context) PushInterruptContinue(string) bool StopImmediate(string) StopWhenIdle() Wait() *adk.TurnLoopExitState[EinoTurnLoopItem, *schema.Message] } type einoTurnLoopRuntimeFactory func(EinoTurnLoopRuntimeConfig) einoTurnLoopRuntimeControl type einoTurnLoopIteratorStarterConfig struct { Context context.Context Agent adk.Agent ConversationID string OrchMode string Progress func(eventType, message string, data interface{}) Logger *zap.Logger Store adk.CheckPointStore CheckPointID string InterruptTimeout time.Duration NativeCancelCause *atomic.Value UnregisterAgentCancel *func() UnregisterTurnLoopInterrupt *func() RuntimeCancelRegistrar AgentRuntimeCancelRegistrar TurnLoopInterruptRegistrar AgentTurnLoopInterruptRegistrar RuntimeFactory einoTurnLoopRuntimeFactory } type einoTurnLoopIteratorStarter struct { cfg einoTurnLoopIteratorStarterConfig } func newEinoTurnLoopIteratorStarter(cfg einoTurnLoopIteratorStarterConfig) *einoTurnLoopIteratorStarter { if cfg.RuntimeFactory == nil { cfg.RuntimeFactory = func(runtimeCfg EinoTurnLoopRuntimeConfig) einoTurnLoopRuntimeControl { return NewEinoTurnLoopRuntime(runtimeCfg) } } return &einoTurnLoopIteratorStarter{cfg: cfg} } func (s *einoTurnLoopIteratorStarter) Start(runMsgs []adk.Message) *adk.AsyncIterator[*adk.AgentEvent] { if s == nil { return nil } callAndClearUnregister(s.cfg.UnregisterTurnLoopInterrupt) callAndClearUnregister(s.cfg.UnregisterAgentCancel) iter, gen := adk.NewAsyncIteratorPair[*adk.AgentEvent]() eventsBridge := newEinoTurnLoopEventBridge(s.cfg.ConversationID, s.cfg.OrchMode, s.cfg.Progress, gen) runtime := s.cfg.RuntimeFactory(EinoTurnLoopRuntimeConfig{ Agent: s.cfg.Agent, InitialMessages: runMsgs, Store: s.cfg.Store, CheckpointID: s.turnLoopCheckpointID(), EnableStreaming: true, InterruptTimeout: s.cfg.InterruptTimeout, OnAgentEvents: eventsBridge.OnAgentEvents, }) s.bindTurnLoopInterrupt(runtime) s.bindRuntimeCancel(runtime) runtime.Run(s.cfg.Context) runtime.StopWhenIdle() go func() { defer gen.Close() state := runtime.Wait() if state == nil || state.ExitReason == nil || eventsBridge.ForwardedError() { return } gen.Send(&adk.AgentEvent{Err: state.ExitReason}) }() return iter } func (s *einoTurnLoopIteratorStarter) turnLoopCheckpointID() string { if s == nil || s.cfg.CheckPointID == "" { return "" } return buildEinoTurnLoopCheckpointID(s.cfg.OrchMode) } func (s *einoTurnLoopIteratorStarter) bindTurnLoopInterrupt(runtime einoTurnLoopRuntimeControl) { if s == nil || runtime == nil || s.cfg.TurnLoopInterruptRegistrar == nil || s.cfg.UnregisterTurnLoopInterrupt == nil { return } *s.cfg.UnregisterTurnLoopInterrupt = s.cfg.TurnLoopInterruptRegistrar(func(note string) bool { ok := runtime.PushInterruptContinue(note) if ok { s.emitInterruptContinueProgress(note) } return ok }) } func (s *einoTurnLoopIteratorStarter) bindRuntimeCancel(runtime einoTurnLoopRuntimeControl) { if s == nil || runtime == nil || s.cfg.RuntimeCancelRegistrar == nil || s.cfg.UnregisterAgentCancel == nil { return } *s.cfg.UnregisterAgentCancel = s.cfg.RuntimeCancelRegistrar(func(cause error) bool { s.storeNativeCancelCause(cause) if errors.Is(cause, ErrInterruptContinue) { return runtime.PushInterruptContinue("") } runtime.StopImmediate("task_cancelled") if s.cfg.Logger != nil { s.cfg.Logger.Info("eino turn loop stop requested", zap.String("conversation_id", s.cfg.ConversationID), zap.String("orchestration", s.cfg.OrchMode), zap.Error(cause)) } return true }) } func (s *einoTurnLoopIteratorStarter) storeNativeCancelCause(cause error) { if s == nil || s.cfg.NativeCancelCause == nil || cause == nil { return } s.cfg.NativeCancelCause.Store(cause) } func (s *einoTurnLoopIteratorStarter) emitInterruptContinueProgress(note string) { if s == nil || s.cfg.Progress == nil { return } trimmed := strings.TrimSpace(note) s.cfg.Progress("user_interrupt_continue", einoTurnLoopInterruptTimelineSummary(note), map[string]interface{}{ "conversationId": s.cfg.ConversationID, "rawReason": trimmed, "emptyReason": trimmed == "", "kind": "turn_loop_preempt", "source": "eino", "orchestration": s.cfg.OrchMode, }) s.cfg.Progress("progress", "已将用户补充推入 Eino TurnLoop,正在等待安全点切换…", map[string]interface{}{ "conversationId": s.cfg.ConversationID, "source": "eino", "orchestration": s.cfg.OrchMode, }) } func callAndClearUnregister(target *func()) { if target == nil || *target == nil { return } (*target)() *target = nil }