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

343 lines
12 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 (
"context"
"errors"
"fmt"
"runtime/debug"
"sync"
"github.com/cloudwego/eino/internal/core"
"github.com/cloudwego/eino/internal/safe"
"github.com/cloudwego/eino/schema"
)
func errorIterator[M MessageType](err error) *AsyncIterator[*TypedAgentEvent[M]] {
iter, gen := NewAsyncIteratorPair[*TypedAgentEvent[M]]()
gen.Send(&TypedAgentEvent[M]{Err: err})
gen.Close()
return iter
}
func newUserMessage[M MessageType](query string) (M, error) {
var zero M
switch any(zero).(type) {
case *schema.Message:
return any(schema.UserMessage(query)).(M), nil
case *schema.AgenticMessage:
return any(schema.UserAgenticMessage(query)).(M), nil
default:
return zero, fmt.Errorf("unsupported message type %T", zero)
}
}
// TypedRunner is the primary entry point for executing an Agent.
// It manages the agent's lifecycle, including starting, resuming, and checkpointing.
//
// Execution always goes through the flowAgent pipeline, which handles
// multi-agent orchestration, callbacks, agent naming, run paths, and cancellation.
type TypedRunner[M MessageType] struct {
a TypedAgent[M]
enableStreaming bool
store CheckPointStore
}
// Runner is the default runner type using *schema.Message.
type Runner = TypedRunner[*schema.Message]
type CheckPointStore = core.CheckPointStore
type CheckPointDeleter = core.CheckPointDeleter
type TypedRunnerConfig[M MessageType] struct {
Agent TypedAgent[M]
EnableStreaming bool
CheckPointStore CheckPointStore
}
// RunnerConfig is the default runner config type using *schema.Message.
type RunnerConfig = TypedRunnerConfig[*schema.Message]
// ResumeParams contains all parameters needed to resume an execution.
// This struct provides an extensible way to pass resume parameters without
// requiring breaking changes to method signatures.
type ResumeParams struct {
// Targets contains the addresses of components to be resumed as keys,
// with their corresponding resume data as values
Targets map[string]any
// Future extensible fields can be added here without breaking changes
}
// NewRunner creates a new Runner with the given config.
func NewRunner(_ context.Context, conf RunnerConfig) *Runner {
return NewTypedRunner(conf)
}
// NewTypedRunner creates a new TypedRunner with the given config.
func NewTypedRunner[M MessageType](conf TypedRunnerConfig[M]) *TypedRunner[M] {
return &TypedRunner[M]{
enableStreaming: conf.EnableStreaming,
a: conf.Agent,
store: conf.CheckPointStore,
}
}
func (r *TypedRunner[M]) Run(ctx context.Context, messages []M,
opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] {
return typedRunnerRunImpl(r.a, r.enableStreaming, r.store, ctx, messages, opts...)
}
// Query is a convenience method that starts a new execution with a single user query string.
func (r *TypedRunner[M]) Query(ctx context.Context,
query string, opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] {
msgs, err := newUserMessage[M](query)
if err != nil {
return errorIterator[M](err)
}
return r.Run(ctx, []M{msgs}, opts...)
}
// Resume continues an interrupted execution from a checkpoint, using an "Implicit Resume All" strategy.
// This method is best for simpler use cases where the act of resuming implies that all previously
// interrupted points should proceed without specific data.
//
// When using this method, all interrupted agents will receive `isResumeFlow = false` when they
// call `GetResumeContext`, as no specific agent was targeted. This is suitable for the "Simple Confirmation"
// pattern where an agent only needs to know `wasInterrupted` is true to continue.
func (r *TypedRunner[M]) Resume(ctx context.Context, checkPointID string, opts ...AgentRunOption) (
*AsyncIterator[*TypedAgentEvent[M]], error) {
return r.resumeInternal(ctx, checkPointID, nil, opts...)
}
// ResumeWithParams continues an interrupted execution from a checkpoint with specific parameters.
// This is the most common and powerful way to resume, allowing you to target specific interrupt points
// (identified by their address/ID) and provide them with data.
//
// The params.Targets map should contain the addresses of the components to be resumed as keys. These addresses
// can point to any interruptible component in the entire execution graph, including ADK agents, compose
// graph nodes, or tools. The value can be the resume data for that component, or `nil` if no data is needed.
//
// When using this method:
// - Components whose addresses are in the params.Targets map will receive `isResumeFlow = true` when they
// call `GetResumeContext`.
// - Interrupted components whose addresses are NOT in the params.Targets map must decide how to proceed:
// -- "Leaf" components (the actual root causes of the original interrupt) MUST re-interrupt themselves
// to preserve their state.
// -- "Composite" agents (like SequentialAgent or ChatModelAgent) should generally proceed with their
// execution. They act as conduits, allowing the resume signal to flow to their children. They will
// naturally re-interrupt if one of their interrupted children re-interrupts, as they receive the
// new `CompositeInterrupt` signal from them.
func (r *TypedRunner[M]) ResumeWithParams(ctx context.Context, checkPointID string, params *ResumeParams, opts ...AgentRunOption) (*AsyncIterator[*TypedAgentEvent[M]], error) {
return r.resumeInternal(ctx, checkPointID, params.Targets, opts...)
}
func (r *TypedRunner[M]) resumeInternal(ctx context.Context, checkPointID string, resumeData map[string]any,
opts ...AgentRunOption) (*AsyncIterator[*TypedAgentEvent[M]], error) {
return typedRunnerResumeInternalImpl(r.a, r.store, ctx, checkPointID, resumeData, opts...)
}
func typedRunnerRunImpl[M MessageType](a TypedAgent[M], enableStreaming bool, store CheckPointStore, ctx context.Context, messages []M, opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]] {
o := getCommonOptions(nil, opts...)
input := &TypedAgentInput[M]{
Messages: messages,
EnableStreaming: enableStreaming,
}
var zero M
if _, ok := any(zero).(*schema.Message); ok {
concreteAgent, _ := any(a).(Agent)
fa := toFlowAgent(ctx, concreteAgent)
if store != nil {
fa.checkPointStore = store
}
concreteInput := any(input).(*AgentInput)
ctx = ctxWithNewTypedRunCtx(ctx, input, o.sharedParentSession)
AddSessionValues(ctx, o.sessionValues)
iter := fa.Run(ctx, concreteInput, opts...)
if store == nil && o.cancelCtx == nil {
return any(iter).(*AsyncIterator[*TypedAgentEvent[M]])
}
niter, gen := NewAsyncIteratorPair[*TypedAgentEvent[M]]()
go typedRunnerHandleIterImpl(enableStreaming, store, ctx, any(iter).(*AsyncIterator[*TypedAgentEvent[M]]), gen, o.checkPointID, o.cancelCtx)
return niter
}
fa := toTypedFlowAgent(a)
if store != nil {
fa.checkPointStore = store
}
ctx = ctxWithNewTypedRunCtx(ctx, input, o.sharedParentSession)
AddSessionValues(ctx, o.sessionValues)
iter := fa.Run(ctx, input, opts...)
if store == nil && o.cancelCtx == nil {
return iter
}
niter, gen := NewAsyncIteratorPair[*TypedAgentEvent[M]]()
go typedRunnerHandleIterImpl(enableStreaming, store, ctx, iter, gen, o.checkPointID, o.cancelCtx)
return niter
}
func typedRunnerResumeInternalImpl[M MessageType](a TypedAgent[M], store CheckPointStore, ctx context.Context, checkPointID string, resumeData map[string]any, //nolint:revive // argument-limit
opts ...AgentRunOption) (*AsyncIterator[*TypedAgentEvent[M]], error) {
if store == nil {
return nil, fmt.Errorf("failed to resume: store is nil")
}
ctx, runCtx, resumeInfo, err := runnerLoadCheckPointImpl(store, ctx, checkPointID)
if err != nil {
return nil, fmt.Errorf("failed to load from checkpoint: %w", err)
}
// Resume uses the streaming mode persisted in the checkpoint, not the value the
// caller passed when constructing the runner. This is the runner's own invariant:
// the checkpoint is the source of truth for what mode the original execution was
// running in, and any new checkpoint written during this resume must preserve it.
enableStreaming := resumeInfo.EnableStreaming
o := getCommonOptions(nil, opts...)
if o.sharedParentSession {
parentSession := getSession(ctx)
if parentSession != nil {
runCtx.Session.Values = parentSession.Values
runCtx.Session.valuesMtx = parentSession.valuesMtx
}
}
if runCtx.Session.valuesMtx == nil {
runCtx.Session.valuesMtx = &sync.Mutex{}
}
if runCtx.Session.Values == nil {
runCtx.Session.Values = make(map[string]any)
}
ctx = setRunCtx(ctx, runCtx)
AddSessionValues(ctx, o.sessionValues)
if len(resumeData) > 0 {
ctx = core.BatchResumeWithData(ctx, resumeData)
}
var zero M
if _, ok := any(zero).(*schema.Message); ok {
concreteAgent, _ := any(a).(Agent)
fa := toFlowAgent(ctx, concreteAgent)
ra, ok := Agent(fa).(ResumableAgent)
if !ok {
return nil, fmt.Errorf("agent %T does not support resume", a)
}
aIter := ra.Resume(ctx, resumeInfo, opts...)
niter, gen := NewAsyncIteratorPair[*TypedAgentEvent[M]]()
go typedRunnerHandleIterImpl(enableStreaming, store, ctx, any(aIter).(*AsyncIterator[*TypedAgentEvent[M]]), gen, &checkPointID, o.cancelCtx)
return niter, nil
}
fa := toTypedFlowAgent(a)
ra, ok := TypedAgent[M](fa).(TypedResumableAgent[M])
if !ok {
return nil, fmt.Errorf("agent %T does not support resume", a)
}
aIter := ra.Resume(ctx, resumeInfo, opts...)
niter, gen := NewAsyncIteratorPair[*TypedAgentEvent[M]]()
go typedRunnerHandleIterImpl(enableStreaming, store, ctx, aIter, gen, &checkPointID, o.cancelCtx)
return niter, nil
}
func typedRunnerHandleIterImpl[M MessageType](enableStreaming bool, store CheckPointStore, ctx context.Context, aIter *AsyncIterator[*TypedAgentEvent[M]], //nolint:revive // argument-limit
gen *AsyncGenerator[*TypedAgentEvent[M]], checkPointID *string, cancelCtx *cancelContext) {
defer func() {
panicErr := recover()
if panicErr != nil {
e := safe.NewPanicErr(panicErr, debug.Stack())
gen.Send(&TypedAgentEvent[M]{Err: e})
}
gen.Close()
}()
var (
interruptSignal *core.InterruptSignal
legacyData any
)
for {
event, ok := aIter.Next()
if !ok {
break
}
if event.Err != nil {
var cancelErr *CancelError
if errors.As(event.Err, &cancelErr) {
if cancelCtx != nil && cancelCtx.isRoot() && cancelCtx.shouldCancel() {
cancelCtx.markCancelHandled()
}
if cancelErr.interruptSignal != nil && checkPointID != nil {
cancelErr.InterruptContexts = core.ToInterruptContexts(cancelErr.interruptSignal, allowedAddressSegmentTypes)
err := runnerSaveCheckPointImpl(enableStreaming, store, ctx, *checkPointID, &InterruptInfo{}, cancelErr.interruptSignal)
if err != nil {
gen.Send(&TypedAgentEvent[M]{Err: fmt.Errorf("failed to save checkpoint on cancel: %w", err)})
}
}
gen.Send(event)
break
}
}
if event.Action != nil && event.Action.internalInterrupted != nil {
if interruptSignal != nil {
panic("multiple interrupt actions should not happen in Runner")
}
interruptSignal = event.Action.internalInterrupted
interruptContexts := core.ToInterruptContexts(interruptSignal, allowedAddressSegmentTypes)
event = &TypedAgentEvent[M]{
AgentName: event.AgentName,
RunPath: event.RunPath,
Output: event.Output,
Action: &AgentAction{
Interrupted: &InterruptInfo{
Data: event.Action.Interrupted.Data,
InterruptContexts: interruptContexts,
},
internalInterrupted: interruptSignal,
},
}
legacyData = event.Action.Interrupted.Data
if checkPointID != nil {
err := runnerSaveCheckPointImpl(enableStreaming, store, ctx, *checkPointID, &InterruptInfo{
Data: legacyData,
}, interruptSignal)
if err != nil {
gen.Send(&TypedAgentEvent[M]{Err: fmt.Errorf("failed to save checkpoint: %w", err)})
}
}
}
gen.Send(event)
}
}