343 lines
12 KiB
Go
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)
|
|
}
|
|
}
|