Files
2026-08-15 02:08:52 +08:00

179 lines
5.7 KiB
Go

package multiagent
import (
"context"
"strings"
"time"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/schema"
)
const (
einoTurnLoopInterruptPreemptTimeout = 3 * time.Second
einoTurnLoopIdleStop = 250 * time.Millisecond
)
// EinoTurnLoopItem is the conversation-level input unit consumed by an Eino
// TurnLoop. The item is gob-friendly so it can be checkpointed by TurnLoop when
// a CheckPointStore is configured.
type EinoTurnLoopItem struct {
Messages []*schema.Message
Kind string
Note string
}
// EinoTurnLoopRuntime wraps Eino's native TurnLoop with the semantics this
// project needs: persistent per-conversation runtime, user-supplement preempt,
// and graceful idle shutdown.
type EinoTurnLoopRuntime struct {
loop *adk.TurnLoop[EinoTurnLoopItem, *schema.Message]
interruptTimeout time.Duration
}
type EinoTurnLoopRuntimeConfig struct {
Agent adk.Agent
InitialMessages []*schema.Message
Store adk.CheckPointStore
CheckpointID string
EnableStreaming bool
PrepareAgent func(context.Context, *adk.TurnLoop[EinoTurnLoopItem, *schema.Message], []EinoTurnLoopItem) (adk.Agent, error)
OnAgentEvents func(context.Context, *adk.TurnContext[EinoTurnLoopItem, *schema.Message], *adk.AsyncIterator[*adk.AgentEvent]) error
InterruptTimeout time.Duration
}
func NewEinoTurnLoopRuntime(cfg EinoTurnLoopRuntimeConfig) *EinoTurnLoopRuntime {
timeout := cfg.InterruptTimeout
if timeout <= 0 {
timeout = einoTurnLoopInterruptPreemptTimeout
}
enableStreaming := cfg.EnableStreaming
prepareAgent := cfg.PrepareAgent
if prepareAgent == nil {
prepareAgent = func(context.Context, *adk.TurnLoop[EinoTurnLoopItem, *schema.Message], []EinoTurnLoopItem) (adk.Agent, error) {
return cfg.Agent, nil
}
}
loop := adk.NewTurnLoop[EinoTurnLoopItem, *schema.Message](adk.TurnLoopConfig[EinoTurnLoopItem, *schema.Message]{
Store: cfg.Store,
CheckpointID: cfg.CheckpointID,
GenInput: func(ctx context.Context, _ *adk.TurnLoop[EinoTurnLoopItem, *schema.Message], items []EinoTurnLoopItem) (*adk.GenInputResult[EinoTurnLoopItem, *schema.Message], error) {
msgs := mergeEinoTurnLoopMessages(items)
return &adk.GenInputResult[EinoTurnLoopItem, *schema.Message]{
RunCtx: ctx,
Input: &adk.AgentInput{
Messages: msgs,
EnableStreaming: enableStreaming,
},
Consumed: items,
}, nil
},
GenResume: func(ctx context.Context, _ *adk.TurnLoop[EinoTurnLoopItem, *schema.Message], interruptedItems, unhandledItems, newItems []EinoTurnLoopItem) (*adk.GenResumeResult[EinoTurnLoopItem, *schema.Message], error) {
consumed := make([]EinoTurnLoopItem, 0, len(interruptedItems)+len(newItems))
consumed = append(consumed, interruptedItems...)
consumed = append(consumed, newItems...)
remaining := append([]EinoTurnLoopItem(nil), unhandledItems...)
return &adk.GenResumeResult[EinoTurnLoopItem, *schema.Message]{
RunCtx: ctx,
Consumed: consumed,
Remaining: remaining,
}, nil
},
PrepareAgent: prepareAgent,
OnAgentEvents: cfg.OnAgentEvents,
})
if len(cfg.InitialMessages) > 0 {
loop.Push(EinoTurnLoopItem{Kind: "initial", Messages: cloneSchemaMessages(cfg.InitialMessages)})
}
return &EinoTurnLoopRuntime{loop: loop, interruptTimeout: timeout}
}
func (r *EinoTurnLoopRuntime) Run(ctx context.Context) {
if r == nil || r.loop == nil {
return
}
r.loop.Run(ctx)
}
func (r *EinoTurnLoopRuntime) PushInterruptContinue(note string) bool {
if r == nil || r.loop == nil {
return false
}
item := EinoTurnLoopItem{
Kind: "interrupt_continue",
Note: strings.TrimSpace(note),
Messages: []*schema.Message{schema.UserMessage(formatInterruptContinuePrompt(note))},
}
ok, ack := r.loop.Push(item, adk.WithPreemptTimeout[EinoTurnLoopItem, *schema.Message](adk.AnySafePoint, r.interruptTimeout))
if ack != nil {
go func() { <-ack }()
}
return ok
}
func (r *EinoTurnLoopRuntime) StopImmediate(cause string) {
if r == nil || r.loop == nil {
return
}
r.loop.Stop(adk.WithImmediate(), adk.WithStopCause(cause))
}
func (r *EinoTurnLoopRuntime) StopWhenIdle() {
if r == nil || r.loop == nil {
return
}
r.loop.Stop(adk.UntilIdleFor(einoTurnLoopIdleStop))
}
func (r *EinoTurnLoopRuntime) Wait() *adk.TurnLoopExitState[EinoTurnLoopItem, *schema.Message] {
if r == nil || r.loop == nil {
return nil
}
return r.loop.Wait()
}
func mergeEinoTurnLoopMessages(items []EinoTurnLoopItem) []*schema.Message {
var msgs []*schema.Message
for _, item := range items {
msgs = append(msgs, cloneSchemaMessages(item.Messages)...)
}
return msgs
}
func formatInterruptContinuePrompt(note string) string {
note = strings.TrimSpace(note)
if note == "" {
return "用户请求中断当前推理并继续。请基于已经完成的步骤继续,不要重复已完成工具调用。"
}
return "用户请求中断当前推理并补充上下文后继续:\n" + note +
"\n\n请基于已经完成的步骤继续,不要重复已完成工具调用。"
}
func cloneSchemaMessages(in []*schema.Message) []*schema.Message {
if len(in) == 0 {
return nil
}
out := make([]*schema.Message, 0, len(in))
for _, msg := range in {
if msg == nil {
continue
}
cp := *msg
if len(msg.ToolCalls) > 0 {
cp.ToolCalls = append([]schema.ToolCall(nil), msg.ToolCalls...)
}
if len(msg.MultiContent) > 0 {
cp.MultiContent = append([]schema.ChatMessagePart(nil), msg.MultiContent...)
}
if len(msg.UserInputMultiContent) > 0 {
cp.UserInputMultiContent = append([]schema.MessageInputPart(nil), msg.UserInputMultiContent...)
}
if len(msg.AssistantGenMultiContent) > 0 {
cp.AssistantGenMultiContent = append([]schema.MessageOutputPart(nil), msg.AssistantGenMultiContent...)
}
cp.Extra = cloneAnyMap(msg.Extra)
out = append(out, &cp)
}
return out
}