Files
2026-07-13 12:49:10 +08:00

568 lines
17 KiB
Go

/*
* Copyright 2025 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package adk
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"io"
"sort"
"sync"
"time"
"github.com/cloudwego/eino/schema"
)
// runSession CheckpointSchema: persisted via serialization.RunCtx (gob).
type runSession struct {
Values map[string]any
valuesMtx *sync.Mutex
Events []*agentEventWrapper
LaneEvents *laneEvents
mtx sync.Mutex
// TypedEvents stores *[]*typedAgentEventWrapper[M] for M != *schema.Message.
// For M = *schema.Message, the existing Events field is used instead.
// The any type is required because Go does not support generic fields in non-generic structs.
TypedEvents any
}
// laneEvents CheckpointSchema: persisted via serialization.RunCtx (gob).
type laneEvents struct {
Events []*agentEventWrapper
Parent *laneEvents
}
// agentEventWrapper CheckpointSchema: persisted via serialization.RunCtx (gob).
type agentEventWrapper struct {
*AgentEvent
mu sync.Mutex
concatenatedMessage Message
// TS is the timestamp (in nanoseconds) when this event was created.
// It is primarily used by the laneEvents mechanism to order events
// from different agents in a multi-agent flow.
TS int64
// StreamErr stores the error message if the MessageStream contained an error.
// This field guards against multiple calls to getMessageFromWrappedEvent
// when the stream has already been consumed and errored.
// Normally when StreamErr happens, the Agent will return with the error,
// unless retry is configured for the agent generating this stream, in which case
// this StreamErr will be of type WillRetryError (indicating retry is pending).
StreamErr error
}
type typedAgentEventWrapper[M MessageType] struct {
event *TypedAgentEvent[M]
mu sync.Mutex
concatenatedMessage M
TS int64
StreamErr error
}
// typedAgentEventWrapperForGob is a gob-serializable representation of typedAgentEventWrapper.
// We encode the event and TS separately to avoid the sync.Mutex and non-exported fields.
type typedAgentEventWrapperForGob[M MessageType] struct {
Event *TypedAgentEvent[M]
TS int64
}
func (e *typedAgentEventWrapper[M]) GobEncode() ([]byte, error) {
if e.event != nil && e.event.Output != nil && e.event.Output.MessageOutput != nil && e.event.Output.MessageOutput.IsStreaming {
// Materialize the stream before encoding.
if isNilMessage(e.concatenatedMessage) && e.StreamErr == nil {
e.consumeStream()
}
}
buf := &bytes.Buffer{}
err := gob.NewEncoder(buf).Encode(&typedAgentEventWrapperForGob[M]{
Event: e.event,
TS: e.TS,
})
if err != nil {
return nil, fmt.Errorf("failed to gob encode generic agent event wrapper: %w", err)
}
return buf.Bytes(), nil
}
func (e *typedAgentEventWrapper[M]) GobDecode(b []byte) error {
g := &typedAgentEventWrapperForGob[M]{}
if err := gob.NewDecoder(bytes.NewReader(b)).Decode(g); err != nil {
return fmt.Errorf("failed to gob decode generic agent event wrapper: %w", err)
}
e.event = g.Event
e.TS = g.TS
return nil
}
// consumeStream drains the typed message stream, setting concatenatedMessage on success
// or StreamErr on failure. The stream is replaced with a materialized version safe for
// gob encoding.
//
// NOTE: This method parallels agentEventWrapper.consumeStream in utils.go. The two
// implementations exist because agentEventWrapper is non-generic (uses *schema.Message
// directly) while typedAgentEventWrapper[M] is generic. They cannot be unified without
// making the non-generic wrapper generic, which would cascade through the entire
// non-generic event storage layer.
func (e *typedAgentEventWrapper[M]) consumeStream() {
e.mu.Lock()
defer e.mu.Unlock()
if !isNilMessage(e.concatenatedMessage) {
return
}
s := e.event.Output.MessageOutput.MessageStream
var msgs []M
defer s.Close()
for {
msg, err := s.Recv()
if err != nil {
if err == io.EOF {
break
}
e.StreamErr = err
e.event.Output.MessageOutput.MessageStream = schema.StreamReaderFromArray(msgs)
return
}
msgs = append(msgs, msg)
}
if len(msgs) == 0 {
e.StreamErr = errors.New("no messages in typedAgentEventWrapper.MessageStream")
e.event.Output.MessageOutput.MessageStream = schema.StreamReaderFromArray(msgs)
return
}
if len(msgs) == 1 {
e.concatenatedMessage = msgs[0]
} else {
var err error
e.concatenatedMessage, err = concatMessageStream(schema.StreamReaderFromArray(msgs))
if err != nil {
e.StreamErr = err
e.event.Output.MessageOutput.MessageStream = schema.StreamReaderFromArray(msgs)
return
}
}
e.event.Output.MessageOutput.MessageStream = schema.StreamReaderFromArray([]M{e.concatenatedMessage})
}
type otherAgentEventWrapperForEncode agentEventWrapper
func (a *agentEventWrapper) GobEncode() ([]byte, error) {
if a.Output != nil && a.Output.MessageOutput != nil && a.Output.MessageOutput.IsStreaming {
// Materialize the stream before encoding. An unconsumed stream that
// ends with a non-EOF error (WillRetryError, ErrStreamCanceled) would
// cause MessageVariant.GobEncode to fail. consumeStream replaces the
// stream with an error-free, materialized version.
if a.concatenatedMessage == nil && a.StreamErr == nil {
a.consumeStream()
}
}
buf := &bytes.Buffer{}
err := gob.NewEncoder(buf).Encode((*otherAgentEventWrapperForEncode)(a))
if err != nil {
return nil, fmt.Errorf("failed to gob encode agent event wrapper: %w", err)
}
return buf.Bytes(), nil
}
func (a *agentEventWrapper) GobDecode(b []byte) error {
return gob.NewDecoder(bytes.NewReader(b)).Decode((*otherAgentEventWrapperForEncode)(a))
}
func newRunSession() *runSession {
return &runSession{
Values: make(map[string]any),
valuesMtx: &sync.Mutex{},
}
}
// GetSessionValues returns all session key-value pairs for the current run.
func GetSessionValues(ctx context.Context) map[string]any {
session := getSession(ctx)
if session == nil {
return map[string]any{}
}
return session.getValues()
}
// AddSessionValue sets a single session key-value pair for the current run.
func AddSessionValue(ctx context.Context, key string, value any) {
session := getSession(ctx)
if session == nil {
return
}
session.addValue(key, value)
}
// AddSessionValues sets multiple session key-value pairs for the current run.
func AddSessionValues(ctx context.Context, kvs map[string]any) {
session := getSession(ctx)
if session == nil {
return
}
session.addValues(kvs)
}
// GetSessionValue retrieves a session value by key and reports whether it exists.
func GetSessionValue(ctx context.Context, key string) (any, bool) {
session := getSession(ctx)
if session == nil {
return nil, false
}
return session.getValue(key)
}
func (rs *runSession) addEvent(event *AgentEvent) {
wrapper := &agentEventWrapper{AgentEvent: event, TS: time.Now().UnixNano()}
// If LaneEvents is not nil, we are in a parallel lane.
// Append to the lane's local event slice (lock-free).
if rs.LaneEvents != nil {
rs.LaneEvents.Events = append(rs.LaneEvents.Events, wrapper)
return
}
// Otherwise, we are on the main path. Append to the shared Events slice (with lock).
rs.mtx.Lock()
rs.Events = append(rs.Events, wrapper)
rs.mtx.Unlock()
}
func (rs *runSession) getEvents() []*agentEventWrapper {
// If there are no in-flight lane events, we can return the main slice directly.
if rs.LaneEvents == nil {
rs.mtx.Lock()
events := rs.Events
rs.mtx.Unlock()
return events
}
// If there are in-flight events, we must construct the full view.
// First, get the committed history from the main Events slice.
rs.mtx.Lock()
committedEvents := make([]*agentEventWrapper, len(rs.Events))
copy(committedEvents, rs.Events)
rs.mtx.Unlock()
// Then, assemble the in-flight events by traversing the linked list.
// Reading the .Parent pointer is safe without a lock because the parent of a lane is immutable after creation.
var laneSlices [][]*agentEventWrapper
totalLaneSize := 0
for lane := rs.LaneEvents; lane != nil; lane = lane.Parent {
if len(lane.Events) > 0 {
laneSlices = append(laneSlices, lane.Events)
totalLaneSize += len(lane.Events)
}
}
// Combine committed and in-flight history.
finalEvents := make([]*agentEventWrapper, 0, len(committedEvents)+totalLaneSize)
finalEvents = append(finalEvents, committedEvents...)
for i := len(laneSlices) - 1; i >= 0; i-- {
finalEvents = append(finalEvents, laneSlices[i]...)
}
return finalEvents
}
func addTypedEvent[M MessageType](session *runSession, event *TypedAgentEvent[M]) {
var zero M
if _, ok := any(zero).(*schema.Message); ok {
session.addEvent(any(event).(*AgentEvent))
return
}
session.mtx.Lock()
defer session.mtx.Unlock()
wrapper := &typedAgentEventWrapper[M]{event: event, TS: time.Now().UnixNano()}
store, _ := session.TypedEvents.(*[]*typedAgentEventWrapper[M])
if store == nil {
s := make([]*typedAgentEventWrapper[M], 0)
store = &s
session.TypedEvents = store
}
*store = append(*store, wrapper)
}
func (rs *runSession) getValues() map[string]any {
rs.valuesMtx.Lock()
values := make(map[string]any, len(rs.Values))
for k, v := range rs.Values {
values[k] = v
}
rs.valuesMtx.Unlock()
return values
}
func (rs *runSession) addValue(key string, value any) {
rs.valuesMtx.Lock()
rs.Values[key] = value
rs.valuesMtx.Unlock()
}
func (rs *runSession) addValues(kvs map[string]any) {
rs.valuesMtx.Lock()
for k, v := range kvs {
rs.Values[k] = v
}
rs.valuesMtx.Unlock()
}
func (rs *runSession) getValue(key string) (any, bool) {
rs.valuesMtx.Lock()
value, ok := rs.Values[key]
rs.valuesMtx.Unlock()
return value, ok
}
type runContext struct {
RootInput *AgentInput
RunPath []RunStep
AgenticRootInput any
Session *runSession
}
func (rc *runContext) isRoot() bool {
return len(rc.RunPath) == 1
}
func (rc *runContext) deepCopy() *runContext {
copied := &runContext{
RootInput: rc.RootInput,
AgenticRootInput: rc.AgenticRootInput,
RunPath: make([]RunStep, len(rc.RunPath)),
Session: rc.Session,
}
copy(copied.RunPath, rc.RunPath)
return copied
}
type runCtxKey struct{}
func getRunCtx(ctx context.Context) *runContext {
runCtx, ok := ctx.Value(runCtxKey{}).(*runContext)
if !ok {
return nil
}
return runCtx
}
func setRunCtx(ctx context.Context, runCtx *runContext) context.Context {
return context.WithValue(ctx, runCtxKey{}, runCtx)
}
func initRunCtx(ctx context.Context, agentName string, input *AgentInput) (context.Context, *runContext) {
runCtx := getRunCtx(ctx)
if runCtx != nil {
runCtx = runCtx.deepCopy()
} else {
runCtx = &runContext{Session: newRunSession()}
}
runCtx.RunPath = append(runCtx.RunPath, RunStep{agentName: agentName})
if runCtx.isRoot() && input != nil {
runCtx.RootInput = input
}
return setRunCtx(ctx, runCtx), runCtx
}
func initTypedRunCtx[M MessageType](ctx context.Context, agentName string, input *TypedAgentInput[M]) (context.Context, *runContext) {
runCtx := getRunCtx(ctx)
if runCtx != nil {
runCtx = runCtx.deepCopy()
} else {
runCtx = &runContext{Session: newRunSession()}
}
runCtx.RunPath = append(runCtx.RunPath, RunStep{agentName: agentName})
if runCtx.isRoot() && input != nil {
var zero M
if _, ok := any(zero).(*schema.Message); ok {
runCtx.RootInput = any(input).(*AgentInput)
} else {
runCtx.AgenticRootInput = input
}
}
return setRunCtx(ctx, runCtx), runCtx
}
func joinRunCtxs(parentCtx context.Context, childCtxs ...context.Context) {
switch len(childCtxs) {
case 0:
return
case 1:
// Optimization for the common case of a single branch.
newEvents := unwindLaneEvents(childCtxs...)
commitEvents(parentCtx, newEvents)
return
}
// 1. Collect all new events from the leaf nodes of each context's lane.
newEvents := unwindLaneEvents(childCtxs...)
// 2. Sort the collected events by their creation timestamp for chronological order.
sort.Slice(newEvents, func(i, j int) bool {
return newEvents[i].TS < newEvents[j].TS
})
// 3. Commit the sorted events to the parent.
commitEvents(parentCtx, newEvents)
}
// commitEvents appends a slice of new events to the correct parent lane or main event log.
func commitEvents(ctx context.Context, newEvents []*agentEventWrapper) {
runCtx := getRunCtx(ctx)
if runCtx == nil || runCtx.Session == nil {
// Should not happen, but handle defensively.
return
}
// If the context we are committing to is itself a lane, append to its event slice.
if runCtx.Session.LaneEvents != nil {
runCtx.Session.LaneEvents.Events = append(runCtx.Session.LaneEvents.Events, newEvents...)
} else {
// Otherwise, commit to the main, shared Events slice with a lock.
runCtx.Session.mtx.Lock()
runCtx.Session.Events = append(runCtx.Session.Events, newEvents...)
runCtx.Session.mtx.Unlock()
}
}
// unwindLaneEvents traverses the LaneEvents of the given contexts and collects
// all events from the leaf nodes.
func unwindLaneEvents(ctxs ...context.Context) []*agentEventWrapper {
var allNewEvents []*agentEventWrapper
for _, ctx := range ctxs {
runCtx := getRunCtx(ctx)
if runCtx != nil && runCtx.Session != nil && runCtx.Session.LaneEvents != nil {
allNewEvents = append(allNewEvents, runCtx.Session.LaneEvents.Events...)
}
}
return allNewEvents
}
func forkRunCtx(ctx context.Context) context.Context {
parentRunCtx := getRunCtx(ctx)
if parentRunCtx == nil || parentRunCtx.Session == nil {
// Should not happen in a parallel workflow, but handle defensively.
return ctx
}
// Create a new session for the child lane by manually copying the parent's session fields.
// This is crucial to ensure a new mutex is created and that the LaneEvents pointer is unique.
childSession := &runSession{
Events: parentRunCtx.Session.Events, // Share the committed history
Values: parentRunCtx.Session.Values, // Share the values map
valuesMtx: parentRunCtx.Session.valuesMtx,
}
// Fork the lane events within the new session struct.
childSession.LaneEvents = &laneEvents{
Parent: parentRunCtx.Session.LaneEvents,
Events: make([]*agentEventWrapper, 0),
}
// Create a new runContext for the child lane, pointing to the new session.
childRunCtx := &runContext{
RootInput: parentRunCtx.RootInput,
RunPath: make([]RunStep, len(parentRunCtx.RunPath)),
Session: childSession,
}
copy(childRunCtx.RunPath, parentRunCtx.RunPath)
return setRunCtx(ctx, childRunCtx)
}
// updateRunPathOnly creates a new context with an updated RunPath, but does NOT modify the Address.
// This is used by sequential workflows to accumulate execution history for LLM context,
// without incorrectly chaining the static addresses of peer agents.
func updateRunPathOnly(ctx context.Context, agentNames ...string) context.Context {
runCtx := getRunCtx(ctx)
if runCtx == nil {
// This should not happen in a sequential workflow context, but handle defensively.
runCtx = &runContext{Session: newRunSession()}
} else {
runCtx = runCtx.deepCopy()
}
for _, agentName := range agentNames {
runCtx.RunPath = append(runCtx.RunPath, RunStep{agentName: agentName})
}
return setRunCtx(ctx, runCtx)
}
// ClearRunCtx clears the run context of the multi-agents. This is particularly useful
// when a customized agent with a multi-agents inside it is set as a subagent of another
// multi-agents. In such cases, it's not expected to pass the outside run context to the
// inside multi-agents, so this function helps isolate the contexts properly.
func ClearRunCtx(ctx context.Context) context.Context {
return context.WithValue(ctx, runCtxKey{}, nil)
}
func ctxWithNewTypedRunCtx[M MessageType](ctx context.Context, input *TypedAgentInput[M], sharedParentSession bool) context.Context {
var session *runSession
if sharedParentSession {
if parentSession := getSession(ctx); parentSession != nil {
session = &runSession{
Values: parentSession.Values,
valuesMtx: parentSession.valuesMtx,
}
}
}
if session == nil {
session = newRunSession()
}
var zero M
rc := &runContext{Session: session}
if _, ok := any(zero).(*schema.Message); ok {
rc.RootInput = any(input).(*AgentInput)
} else {
rc.AgenticRootInput = input
}
return setRunCtx(ctx, rc)
}
func getSession(ctx context.Context) *runSession {
runCtx := getRunCtx(ctx)
if runCtx != nil {
return runCtx.Session
}
return nil
}