/* * Copyright 2024 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 compose import ( "context" "fmt" "reflect" "github.com/cloudwego/eino/internal/core" "github.com/cloudwego/eino/internal/serialization" "github.com/cloudwego/eino/schema" ) func init() { schema.RegisterName[*checkpoint]("_eino_checkpoint") schema.RegisterName[*dagChannel]("_eino_dag_channel") schema.RegisterName[*pregelChannel]("_eino_pregel_channel") schema.RegisterName[dependencyState]("_eino_dependency_state") _ = serialization.GenericRegister[channel]("_eino_channel") } // RegisterSerializableType registers a custom type for eino serialization. // This allows eino to properly serialize and deserialize custom types. // Both custom interfaces and structs need to be registered using this function. // Types only need to be registered once - pointers and other references will be handled automatically. // All built-in eino types are already registered. // Parameters: // - name: A unique identifier for the type being registered (should not start with "_eino") // - T: The generic type parameter representing the type to register // Returns: // - error: An error if registration fails (e.g., if the type is already registered) // Deprecated: RegisterSerializableType is deprecated. Use schema.RegisterName[T](name) instead. func RegisterSerializableType[T any](name string) (err error) { return serialization.GenericRegister[T](name) } type CheckPointStore = core.CheckPointStore type Serializer interface { Marshal(v any) ([]byte, error) Unmarshal(data []byte, v any) error } // WithCheckPointStore sets the checkpoint store implementation for a graph. func WithCheckPointStore(store CheckPointStore) GraphCompileOption { return func(o *graphCompileOptions) { o.checkPointStore = store } } // WithSerializer sets the serializer used to persist checkpoint state. func WithSerializer(serializer Serializer) GraphCompileOption { return func(o *graphCompileOptions) { o.serializer = serializer } } // WithCheckPointID sets the checkpoint ID to load from and write to by default. func WithCheckPointID(checkPointID string) Option { return Option{ checkPointID: &checkPointID, } } // WithWriteToCheckPointID specifies a different checkpoint ID to write to. // If not provided, the checkpoint ID from WithCheckPointID will be used for writing. // This is useful for scenarios where you want to load from an existed checkpoint // but save the progress to a new, separate checkpoint. func WithWriteToCheckPointID(checkPointID string) Option { return Option{ writeToCheckPointID: &checkPointID, } } // WithForceNewRun forces the graph to run from the beginning, ignoring any checkpoints. func WithForceNewRun() Option { return Option{ forceNewRun: true, } } // StateModifier modifies state during checkpoint operations for a given node path. type StateModifier func(ctx context.Context, path NodePath, state any) error // WithStateModifier installs a state modifier invoked during checkpoint read/write. func WithStateModifier(sm StateModifier) Option { return Option{ stateModifier: sm, } } type checkpoint struct { Channels map[string]channel Inputs map[string] /*node key*/ any /*input*/ State any SkipPreHandler map[string]bool RerunNodes []string SubGraphs map[string]*checkpoint InterruptID2Addr map[string]Address InterruptID2State map[string]core.InterruptState } type stateModifierKey struct{} type checkPointKey struct{} // *checkpoint func getStateModifier(ctx context.Context) StateModifier { if sm, ok := ctx.Value(stateModifierKey{}).(StateModifier); ok { return sm } return nil } func setStateModifier(ctx context.Context, modifier StateModifier) context.Context { return context.WithValue(ctx, stateModifierKey{}, modifier) } func getCheckPointFromStore(ctx context.Context, id string, cpr *checkPointer) (cp *checkpoint, err error) { cp, existed, err := cpr.get(ctx, id) if err != nil { return nil, err } if !existed { return nil, nil } return cp, nil } func setCheckPointToCtx(ctx context.Context, cp *checkpoint) context.Context { ctx = core.PopulateInterruptState(ctx, cp.InterruptID2Addr, cp.InterruptID2State) return context.WithValue(ctx, checkPointKey{}, cp) } func getCheckPointFromCtx(ctx context.Context) *checkpoint { if cp, ok := ctx.Value(checkPointKey{}).(*checkpoint); ok { return cp } return nil } func forwardCheckPoint(ctx context.Context, nodeKey string) context.Context { cp := getCheckPointFromCtx(ctx) if cp == nil { return ctx } if subCP, ok := cp.SubGraphs[nodeKey]; ok { delete(cp.SubGraphs, nodeKey) // only forward once return context.WithValue(ctx, checkPointKey{}, subCP) } return context.WithValue(ctx, checkPointKey{}, (*checkpoint)(nil)) } func newCheckPointer( inputPairs, outputPairs map[string]streamConvertPair, store CheckPointStore, serializer Serializer, ) *checkPointer { if serializer == nil { serializer = &serialization.InternalSerializer{} } return &checkPointer{ sc: newStreamConverter(inputPairs, outputPairs), store: store, serializer: serializer, } } type checkPointer struct { sc *streamConverter store CheckPointStore serializer Serializer } func (c *checkPointer) get(ctx context.Context, id string) (*checkpoint, bool, error) { data, existed, err := c.store.Get(ctx, id) if err != nil || existed == false { return nil, existed, err } cp := &checkpoint{} err = c.serializer.Unmarshal(data, cp) if err != nil { return nil, false, err } return cp, true, nil } func (c *checkPointer) set(ctx context.Context, id string, cp *checkpoint) error { normalizeCheckpointTypedNilInputs(cp) data, err := c.serializer.Marshal(cp) if err != nil { return err } return c.store.Set(ctx, id, data) } func normalizeCheckpointTypedNilInputs(cp *checkpoint) { if cp == nil { return } for key, input := range cp.Inputs { if isTypedNil(input) { cp.Inputs[key] = nil } } for _, sub := range cp.SubGraphs { normalizeCheckpointTypedNilInputs(sub) } } func isTypedNil(v any) bool { if v == nil { return false } rv := reflect.ValueOf(v) switch rv.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: return rv.IsNil() default: return false } } // MigrateCheckpointState is an advanced compatibility utility for checkpoint upgrades. // // It decodes checkpoint bytes using the given serializer, applies migrate to checkpoint.State and // all nested SubGraphs' states, then re-encodes the checkpoint. // // Typical use cases: // - Resume-time migration when you changed your graph state type/schema and need to load old // checkpoints without discarding them. // - Framework-level backward compatibility (e.g. ADK upgrading checkpoints across versions). // // Migrate callback contract: // - Returns (newState, changed, error). // - If changed is false, the state is left as-is. // - If error is non-nil, migration stops and the error is returned to the caller. // // The original bytes are returned only if no state was changed anywhere in the checkpoint tree. func MigrateCheckpointState(data []byte, serializer Serializer, migrate func(state any) (any, bool, error)) ([]byte, error) { cp := &checkpoint{} if err := serializer.Unmarshal(data, cp); err != nil { return nil, err } changed, err := migrateCheckpoint(cp, migrate) if err != nil { return nil, err } if !changed { return data, nil } return serializer.Marshal(cp) } // migrateCheckpoint recursively applies migrate to cp.State and all SubGraphs. func migrateCheckpoint(cp *checkpoint, migrate func(state any) (any, bool, error)) (bool, error) { anyChanged := false if cp.State != nil { newState, changed, err := migrate(cp.State) if err != nil { return false, err } if changed { cp.State = newState anyChanged = true } } for _, sub := range cp.SubGraphs { changed, err := migrateCheckpoint(sub, migrate) if err != nil { return false, err } if changed { anyChanged = true } } return anyChanged, nil } // convertCheckPoint if value in checkpoint is streamReader, convert it to non-stream func (c *checkPointer) convertCheckPoint(cp *checkpoint, isStream bool) (err error) { for _, ch := range cp.Channels { err = ch.convertValues(func(m map[string]any) error { return c.sc.convertOutputs(isStream, m) }) if err != nil { return err } } err = c.sc.convertInputs(isStream, cp.Inputs) if err != nil { return err } return nil } // convertCheckPoint convert values in checkpoint to streamReader if needed func (c *checkPointer) restoreCheckPoint(cp *checkpoint, isStream bool) (err error) { for _, ch := range cp.Channels { err = ch.convertValues(func(m map[string]any) error { return c.sc.restoreOutputs(isStream, m) }) if err != nil { return err } } err = c.sc.restoreInputs(isStream, cp.Inputs) if err != nil { return err } return nil } func newStreamConverter(inputPairs, outputPairs map[string]streamConvertPair) *streamConverter { return &streamConverter{ inputPairs: inputPairs, outputPairs: outputPairs, } } type streamConverter struct { inputPairs, outputPairs map[string]streamConvertPair } func (s *streamConverter) convertInputs(isStream bool, values map[string]any) error { return convert(values, s.inputPairs, isStream) } func (s *streamConverter) restoreInputs(isStream bool, values map[string]any) error { return restore(values, s.inputPairs, isStream) } func (s *streamConverter) convertOutputs(isStream bool, values map[string]any) error { return convert(values, s.outputPairs, isStream) } func (s *streamConverter) restoreOutputs(isStream bool, values map[string]any) error { return restore(values, s.outputPairs, isStream) } func convert(values map[string]any, convPairs map[string]streamConvertPair, isStream bool) error { if !isStream { return nil } for key, v := range values { convPair, ok := convPairs[key] if !ok { return fmt.Errorf("checkpoint conv stream fail, node[%s] have not been registered", key) } sr, ok := v.(streamReader) if !ok { return fmt.Errorf("checkpoint conv stream fail, value of [%s] isn't stream", key) } nValue, err := convPair.concatStream(sr) if err != nil { return err } values[key] = nValue } return nil } func restore(values map[string]any, convPairs map[string]streamConvertPair, isStream bool) error { if !isStream { return nil } for key, v := range values { convPair, ok := convPairs[key] if !ok { return fmt.Errorf("checkpoint restore stream fail, node[%s] have not been registered", key) } sr, err := convPair.restoreStream(v) if err != nil { return err } values[key] = sr } return nil }