732 lines
22 KiB
Go
732 lines
22 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"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/cloudwego/eino/schema"
|
|
)
|
|
|
|
type dtTestStore struct {
|
|
data map[string][]byte
|
|
}
|
|
|
|
func newDTTestStore() *dtTestStore {
|
|
return &dtTestStore{data: make(map[string][]byte)}
|
|
}
|
|
|
|
func (s *dtTestStore) Set(_ context.Context, key string, value []byte) error {
|
|
s.data[key] = value
|
|
return nil
|
|
}
|
|
|
|
func (s *dtTestStore) Get(_ context.Context, key string) ([]byte, bool, error) {
|
|
v, ok := s.data[key]
|
|
return v, ok, nil
|
|
}
|
|
|
|
type dtTestAgent struct {
|
|
name string
|
|
runFn func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent]
|
|
resumeFn func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent]
|
|
}
|
|
|
|
func (a *dtTestAgent) Name(_ context.Context) string {
|
|
return a.name
|
|
}
|
|
|
|
func (a *dtTestAgent) Description(_ context.Context) string {
|
|
return a.name + " description"
|
|
}
|
|
|
|
func (a *dtTestAgent) Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
return a.runFn(ctx, input, options...)
|
|
}
|
|
|
|
func (a *dtTestAgent) Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
if a.resumeFn != nil {
|
|
return a.resumeFn(ctx, info, opts...)
|
|
}
|
|
return a.runFn(ctx, &AgentInput{}, opts...)
|
|
}
|
|
|
|
func TestDeterministicTransferFlowAgentInterruptResume(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newDTTestStore()
|
|
|
|
interruptData := "interrupt_data"
|
|
var runCount int
|
|
|
|
innerAgent := &dtTestAgent{
|
|
name: "inner",
|
|
runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
runCount++
|
|
iter, gen := NewAsyncIteratorPair[*AgentEvent]()
|
|
go func() {
|
|
defer gen.Close()
|
|
gen.Send(EventFromMessage(schema.AssistantMessage("before interrupt", nil), nil, schema.Assistant, ""))
|
|
intEvent := Interrupt(ctx, interruptData)
|
|
intEvent.Action.Interrupted.Data = interruptData
|
|
gen.Send(intEvent)
|
|
}()
|
|
return iter
|
|
},
|
|
resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
runCount++
|
|
|
|
assert.True(t, info.WasInterrupted, "innerAgent resumeFn: should be interrupted")
|
|
assert.True(t, info.IsResumeTarget, "innerAgent resumeFn: should be resume target")
|
|
|
|
runCtx := getRunCtx(ctx)
|
|
assert.NotNil(t, runCtx, "innerAgent resumeFn: runCtx should not be nil")
|
|
assert.NotNil(t, runCtx.Session, "innerAgent resumeFn: runCtx.Session should not be nil")
|
|
|
|
var agentEvents []*AgentEvent
|
|
for _, ev := range runCtx.Session.Events {
|
|
if ev.AgentEvent != nil {
|
|
agentEvents = append(agentEvents, ev.AgentEvent)
|
|
}
|
|
}
|
|
|
|
assert.Len(t, agentEvents, 1, "innerAgent resumeFn: should have exactly 1 agent event")
|
|
if len(agentEvents) == 1 {
|
|
ev := agentEvents[0]
|
|
assert.Equal(t, "inner", ev.AgentName, "innerAgent resumeFn: event should be from inner agent")
|
|
assert.Equal(t, "before interrupt", ev.Output.MessageOutput.Message.Content, "innerAgent resumeFn: event content should be 'before interrupt'")
|
|
assert.Len(t, ev.RunPath, 2, "innerAgent resumeFn: RunPath should have 2 steps (outer agent, inner agent)")
|
|
if len(ev.RunPath) == 2 {
|
|
assert.Equal(t, "outer", ev.RunPath[0].agentName, "innerAgent resumeFn: RunPath[0] should be outer agent")
|
|
assert.Equal(t, "inner", ev.RunPath[1].agentName, "innerAgent resumeFn: RunPath[1] should be inner agent")
|
|
}
|
|
}
|
|
|
|
iter, gen := NewAsyncIteratorPair[*AgentEvent]()
|
|
go func() {
|
|
defer gen.Close()
|
|
gen.Send(EventFromMessage(schema.AssistantMessage("after resume", nil), nil, schema.Assistant, ""))
|
|
}()
|
|
return iter
|
|
},
|
|
}
|
|
|
|
innerFlowAgent := toFlowAgent(ctx, innerAgent)
|
|
|
|
wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{
|
|
Agent: innerFlowAgent,
|
|
ToAgentNames: []string{"next_agent"},
|
|
})
|
|
|
|
outerAgent := &dtTestAgent{
|
|
name: "outer",
|
|
runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
return wrapped.Run(ctx, input, options...)
|
|
},
|
|
resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
assert.True(t, info.WasInterrupted, "outerAgent resumeFn: should be interrupted")
|
|
|
|
runCtx := getRunCtx(ctx)
|
|
assert.NotNil(t, runCtx, "outerAgent resumeFn: runCtx should not be nil")
|
|
assert.NotNil(t, runCtx.Session, "outerAgent resumeFn: runCtx.Session should not be nil")
|
|
|
|
var agentEvents []*AgentEvent
|
|
for _, ev := range runCtx.Session.Events {
|
|
if ev.AgentEvent != nil {
|
|
agentEvents = append(agentEvents, ev.AgentEvent)
|
|
}
|
|
}
|
|
|
|
assert.Len(t, agentEvents, 1, "outerAgent resumeFn: should have exactly 1 agent event")
|
|
if len(agentEvents) == 1 {
|
|
ev := agentEvents[0]
|
|
assert.Equal(t, "inner", ev.AgentName, "outerAgent resumeFn: event should be from inner agent (preserved original)")
|
|
assert.Equal(t, "before interrupt", ev.Output.MessageOutput.Message.Content, "outerAgent resumeFn: event content should be 'before interrupt'")
|
|
assert.Len(t, ev.RunPath, 2, "outerAgent resumeFn: RunPath should have 2 steps")
|
|
if len(ev.RunPath) == 2 {
|
|
assert.Equal(t, "outer", ev.RunPath[0].agentName, "outerAgent resumeFn: RunPath[0] should be outer agent")
|
|
assert.Equal(t, "inner", ev.RunPath[1].agentName, "outerAgent resumeFn: RunPath[1] should be inner agent")
|
|
}
|
|
}
|
|
|
|
ra := wrapped.(ResumableAgent)
|
|
return ra.Resume(ctx, info, opts...)
|
|
},
|
|
}
|
|
|
|
outerFlowAgent := toFlowAgent(ctx, outerAgent)
|
|
|
|
runner := NewRunner(ctx, RunnerConfig{
|
|
Agent: outerFlowAgent,
|
|
EnableStreaming: true,
|
|
CheckPointStore: store,
|
|
})
|
|
|
|
iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, WithCheckPointID("cp1"))
|
|
|
|
var events []*AgentEvent
|
|
var interrupted bool
|
|
var interruptEvent *AgentEvent
|
|
for {
|
|
ev, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
events = append(events, ev)
|
|
if ev.Action != nil && ev.Action.Interrupted != nil {
|
|
interrupted = true
|
|
interruptEvent = ev
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, 1, runCount, "run should have been called once")
|
|
assert.True(t, interrupted, "should have interrupted")
|
|
assert.Greater(t, len(events), 0, "should have events")
|
|
if interruptEvent == nil {
|
|
t.Fatal("should have interrupt event")
|
|
}
|
|
assert.NotEmpty(t, interruptEvent.Action.Interrupted.InterruptContexts, "should have interrupt contexts")
|
|
|
|
_, exists, err := store.Get(ctx, "cp1")
|
|
assert.NoError(t, err)
|
|
assert.True(t, exists, "checkpoint should have been saved")
|
|
|
|
var hasDeterministicTransferContext bool
|
|
for _, intCtx := range interruptEvent.Action.Interrupted.InterruptContexts {
|
|
t.Logf("InterruptContext: ID=%s, Info=%v, IsRootCause=%v, Addr=%v", intCtx.ID, intCtx.Info, intCtx.IsRootCause, intCtx.Address)
|
|
if intCtx.Info == "deterministic transfer wrapper interrupted" {
|
|
hasDeterministicTransferContext = true
|
|
}
|
|
for parent := intCtx.Parent; parent != nil; parent = parent.Parent {
|
|
t.Logf(" Parent: ID=%s, Info=%v, Addr=%v", parent.ID, parent.Info, parent.Address)
|
|
if parent.Info == "deterministic transfer wrapper interrupted" {
|
|
hasDeterministicTransferContext = true
|
|
}
|
|
}
|
|
}
|
|
assert.True(t, hasDeterministicTransferContext, "should have deterministic transfer interrupt context")
|
|
|
|
var rootCauseID string
|
|
for _, intCtx := range interruptEvent.Action.Interrupted.InterruptContexts {
|
|
if intCtx.IsRootCause {
|
|
rootCauseID = intCtx.ID
|
|
break
|
|
}
|
|
}
|
|
assert.NotEmpty(t, rootCauseID, "should have root cause interrupt ID")
|
|
|
|
resumeIter, err := runner.ResumeWithParams(ctx, "cp1", &ResumeParams{
|
|
Targets: map[string]any{rootCauseID: nil},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
var resumeEvents []*AgentEvent
|
|
var resumeErr error
|
|
var hasTransfer bool
|
|
for {
|
|
ev, ok := resumeIter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
|
|
if ev.Err != nil {
|
|
resumeErr = ev.Err
|
|
}
|
|
if ev.Action != nil && ev.Action.TransferToAgent != nil {
|
|
hasTransfer = true
|
|
}
|
|
resumeEvents = append(resumeEvents, ev)
|
|
}
|
|
|
|
assert.Equal(t, 2, runCount, "inner agent should be called twice (once for initial, once for resume)")
|
|
assert.NotEmpty(t, resumeEvents, "should have resume events")
|
|
assert.True(t, hasTransfer, "should have transfer action after resume")
|
|
assert.Error(t, resumeErr, "transfer should fail because next_agent doesn't exist")
|
|
assert.Contains(t, resumeErr.Error(), "next_agent", "error should mention the missing agent")
|
|
}
|
|
|
|
func TestDeterministicTransferRunPathPreserved(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newDTTestStore()
|
|
|
|
var collectedRunPaths [][]RunStep
|
|
|
|
innerAgent := &dtTestAgent{
|
|
name: "inner",
|
|
runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
iter, gen := NewAsyncIteratorPair[*AgentEvent]()
|
|
go func() {
|
|
defer gen.Close()
|
|
ev := EventFromMessage(schema.AssistantMessage("from inner", nil), nil, schema.Assistant, "")
|
|
gen.Send(ev)
|
|
}()
|
|
return iter
|
|
},
|
|
}
|
|
|
|
innerFlowAgent := toFlowAgent(ctx, innerAgent)
|
|
|
|
wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{
|
|
Agent: innerFlowAgent,
|
|
ToAgentNames: []string{},
|
|
})
|
|
|
|
outerAgent := &dtTestAgent{
|
|
name: "outer",
|
|
runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
innerIter := wrapped.Run(ctx, input, options...)
|
|
iter, gen := NewAsyncIteratorPair[*AgentEvent]()
|
|
go func() {
|
|
defer gen.Close()
|
|
for {
|
|
ev, ok := innerIter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
collectedRunPaths = append(collectedRunPaths, ev.RunPath)
|
|
gen.Send(ev)
|
|
}
|
|
}()
|
|
return iter
|
|
},
|
|
}
|
|
|
|
outerFlowAgent := toFlowAgent(ctx, outerAgent)
|
|
|
|
runner := NewRunner(ctx, RunnerConfig{
|
|
Agent: outerFlowAgent,
|
|
EnableStreaming: true,
|
|
CheckPointStore: store,
|
|
})
|
|
|
|
iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, WithCheckPointID("cp1"))
|
|
|
|
for {
|
|
_, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
}
|
|
|
|
assert.NotEmpty(t, collectedRunPaths, "should have collected RunPaths")
|
|
for _, rp := range collectedRunPaths {
|
|
assert.Len(t, rp, 2, "RunPath should have 2 steps (outer agent, inner agent)")
|
|
if len(rp) == 2 {
|
|
assert.Equal(t, "outer", rp[0].agentName, "RunPath[0] should be outer agent")
|
|
assert.Equal(t, "inner", rp[1].agentName, "RunPath[1] should be inner agent")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeterministicTransferExitSkipsTransfer(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newDTTestStore()
|
|
|
|
innerAgent := &dtTestAgent{
|
|
name: "inner",
|
|
runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
iter, gen := NewAsyncIteratorPair[*AgentEvent]()
|
|
go func() {
|
|
defer gen.Close()
|
|
ev := EventFromMessage(schema.AssistantMessage("inner exits", nil), nil, schema.Assistant, "")
|
|
ev.Action = &AgentAction{Exit: true}
|
|
gen.Send(ev)
|
|
}()
|
|
return iter
|
|
},
|
|
}
|
|
|
|
innerFlowAgent := toFlowAgent(ctx, innerAgent)
|
|
|
|
wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{
|
|
Agent: innerFlowAgent,
|
|
ToAgentNames: []string{"next_agent"},
|
|
})
|
|
|
|
var outerSawExit bool
|
|
var transferGenerated bool
|
|
|
|
outerAgent := &dtTestAgent{
|
|
name: "outer",
|
|
runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
innerIter := wrapped.Run(ctx, input, options...)
|
|
iter, gen := NewAsyncIteratorPair[*AgentEvent]()
|
|
go func() {
|
|
defer gen.Close()
|
|
for {
|
|
ev, ok := innerIter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if ev.Action != nil && ev.Action.Exit {
|
|
outerSawExit = true
|
|
}
|
|
if ev.Action != nil && ev.Action.TransferToAgent != nil {
|
|
transferGenerated = true
|
|
}
|
|
gen.Send(ev)
|
|
}
|
|
}()
|
|
return iter
|
|
},
|
|
}
|
|
|
|
outerFlowAgent := toFlowAgent(ctx, outerAgent)
|
|
|
|
runner := NewRunner(ctx, RunnerConfig{
|
|
Agent: outerFlowAgent,
|
|
EnableStreaming: true,
|
|
CheckPointStore: store,
|
|
})
|
|
|
|
iter := runner.Run(ctx, []Message{schema.UserMessage("test")}, WithCheckPointID("cp1"))
|
|
|
|
for {
|
|
_, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
}
|
|
|
|
assert.True(t, outerSawExit, "outer should see exit event from inner")
|
|
assert.False(t, transferGenerated, "transfer should not be generated when inner exits")
|
|
}
|
|
|
|
type nonFlowTestAgent struct {
|
|
name string
|
|
runFn func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent]
|
|
resumeFn func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent]
|
|
}
|
|
|
|
func (a *nonFlowTestAgent) Name(_ context.Context) string {
|
|
return a.name
|
|
}
|
|
|
|
func (a *nonFlowTestAgent) Description(_ context.Context) string {
|
|
return a.name + " description"
|
|
}
|
|
|
|
func (a *nonFlowTestAgent) Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
return a.runFn(ctx, input, options...)
|
|
}
|
|
|
|
func (a *nonFlowTestAgent) Resume(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
if a.resumeFn != nil {
|
|
return a.resumeFn(ctx, info, opts...)
|
|
}
|
|
return a.runFn(ctx, &AgentInput{}, opts...)
|
|
}
|
|
|
|
type nonResumableTestAgent struct {
|
|
name string
|
|
runFn func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent]
|
|
}
|
|
|
|
func (a *nonResumableTestAgent) Name(_ context.Context) string {
|
|
return a.name
|
|
}
|
|
|
|
func (a *nonResumableTestAgent) Description(_ context.Context) string {
|
|
return a.name + " description"
|
|
}
|
|
|
|
func (a *nonResumableTestAgent) Run(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
return a.runFn(ctx, input, options...)
|
|
}
|
|
|
|
func TestDeterministicTransferNonFlowAgent_ExitSkipsTransfer(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
agent := &nonFlowTestAgent{
|
|
name: "test_agent",
|
|
runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
iter, gen := NewAsyncIteratorPair[*AgentEvent]()
|
|
go func() {
|
|
defer gen.Close()
|
|
ev := EventFromMessage(schema.AssistantMessage("exiting", nil), nil, schema.Assistant, "")
|
|
ev.Action = &AgentAction{Exit: true}
|
|
gen.Send(ev)
|
|
}()
|
|
return iter
|
|
},
|
|
}
|
|
|
|
wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{
|
|
Agent: agent,
|
|
ToAgentNames: []string{"next_agent"},
|
|
})
|
|
|
|
iter := wrapped.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}})
|
|
|
|
var events []*AgentEvent
|
|
var sawExit bool
|
|
var sawTransfer bool
|
|
for {
|
|
ev, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
events = append(events, ev)
|
|
if ev.Action != nil && ev.Action.Exit {
|
|
sawExit = true
|
|
}
|
|
if ev.Action != nil && ev.Action.TransferToAgent != nil {
|
|
sawTransfer = true
|
|
}
|
|
}
|
|
|
|
assert.True(t, sawExit, "should see exit event")
|
|
assert.False(t, sawTransfer, "should NOT see transfer when exit is last event")
|
|
assert.Len(t, events, 1, "should have exactly 1 event (exit)")
|
|
}
|
|
|
|
func TestDeterministicTransferNonFlowAgent_AppendsTransfer(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
agent := &nonFlowTestAgent{
|
|
name: "test_agent",
|
|
runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
iter, gen := NewAsyncIteratorPair[*AgentEvent]()
|
|
go func() {
|
|
defer gen.Close()
|
|
ev := EventFromMessage(schema.AssistantMessage("normal output", nil), nil, schema.Assistant, "")
|
|
gen.Send(ev)
|
|
}()
|
|
return iter
|
|
},
|
|
}
|
|
|
|
wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{
|
|
Agent: agent,
|
|
ToAgentNames: []string{"next_agent"},
|
|
})
|
|
|
|
iter := wrapped.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}})
|
|
|
|
var events []*AgentEvent
|
|
var sawTransfer bool
|
|
var transferTarget string
|
|
for {
|
|
ev, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
events = append(events, ev)
|
|
if ev.Action != nil && ev.Action.TransferToAgent != nil {
|
|
sawTransfer = true
|
|
transferTarget = ev.Action.TransferToAgent.DestAgentName
|
|
}
|
|
}
|
|
|
|
assert.True(t, sawTransfer, "should see transfer event after normal completion")
|
|
assert.Equal(t, "next_agent", transferTarget, "transfer target should be next_agent")
|
|
assert.Greater(t, len(events), 1, "should have more than 1 event (output + transfer messages)")
|
|
}
|
|
|
|
func TestDeterministicTransferNonFlowAgent_InterruptSkipsTransfer(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
agent := &nonFlowTestAgent{
|
|
name: "test_agent",
|
|
runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
iter, gen := NewAsyncIteratorPair[*AgentEvent]()
|
|
go func() {
|
|
defer gen.Close()
|
|
ev := &AgentEvent{
|
|
Action: &AgentAction{
|
|
Interrupted: &InterruptInfo{Data: "test interrupt"},
|
|
},
|
|
}
|
|
gen.Send(ev)
|
|
}()
|
|
return iter
|
|
},
|
|
}
|
|
|
|
wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{
|
|
Agent: agent,
|
|
ToAgentNames: []string{"next_agent"},
|
|
})
|
|
|
|
iter := wrapped.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}})
|
|
|
|
var events []*AgentEvent
|
|
var sawInterrupt bool
|
|
var sawTransfer bool
|
|
for {
|
|
ev, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
events = append(events, ev)
|
|
if ev.Action != nil && ev.Action.Interrupted != nil {
|
|
sawInterrupt = true
|
|
}
|
|
if ev.Action != nil && ev.Action.TransferToAgent != nil {
|
|
sawTransfer = true
|
|
}
|
|
}
|
|
|
|
assert.True(t, sawInterrupt, "should see interrupt event")
|
|
assert.False(t, sawTransfer, "should NOT see transfer when interrupted")
|
|
assert.Len(t, events, 1, "should have exactly 1 event (interrupt)")
|
|
}
|
|
|
|
func TestDeterministicTransferNonFlowAgent_Resume(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
var resumeCalled bool
|
|
agent := &nonFlowTestAgent{
|
|
name: "test_agent",
|
|
runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
iter, gen := NewAsyncIteratorPair[*AgentEvent]()
|
|
go func() {
|
|
defer gen.Close()
|
|
ev := EventFromMessage(schema.AssistantMessage("from run", nil), nil, schema.Assistant, "")
|
|
gen.Send(ev)
|
|
}()
|
|
return iter
|
|
},
|
|
resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
resumeCalled = true
|
|
iter, gen := NewAsyncIteratorPair[*AgentEvent]()
|
|
go func() {
|
|
defer gen.Close()
|
|
ev := EventFromMessage(schema.AssistantMessage("from resume", nil), nil, schema.Assistant, "")
|
|
gen.Send(ev)
|
|
}()
|
|
return iter
|
|
},
|
|
}
|
|
|
|
wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{
|
|
Agent: agent,
|
|
ToAgentNames: []string{"next_agent"},
|
|
})
|
|
|
|
ra, ok := wrapped.(ResumableAgent)
|
|
assert.True(t, ok, "wrapped agent should be ResumableAgent")
|
|
|
|
iter := ra.Resume(ctx, &ResumeInfo{WasInterrupted: true})
|
|
|
|
var sawTransfer bool
|
|
for {
|
|
ev, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if ev.Action != nil && ev.Action.TransferToAgent != nil {
|
|
sawTransfer = true
|
|
}
|
|
}
|
|
|
|
assert.True(t, resumeCalled, "resume should have been called on inner agent")
|
|
assert.True(t, sawTransfer, "should see transfer event after resume completion")
|
|
}
|
|
|
|
func TestDeterministicTransferFlowAgent_ResumeWithInvalidState(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
innerAgent := &dtTestAgent{
|
|
name: "inner",
|
|
runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
iter, gen := NewAsyncIteratorPair[*AgentEvent]()
|
|
go func() {
|
|
defer gen.Close()
|
|
gen.Send(EventFromMessage(schema.AssistantMessage("test", nil), nil, schema.Assistant, ""))
|
|
}()
|
|
return iter
|
|
},
|
|
}
|
|
|
|
innerFlowAgent := toFlowAgent(ctx, innerAgent)
|
|
|
|
wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{
|
|
Agent: innerFlowAgent,
|
|
ToAgentNames: []string{"next_agent"},
|
|
})
|
|
|
|
ra, ok := wrapped.(ResumableAgent)
|
|
assert.True(t, ok, "wrapped flowAgent should be ResumableAgent")
|
|
|
|
iter := ra.Resume(ctx, &ResumeInfo{
|
|
WasInterrupted: true,
|
|
InterruptState: nil,
|
|
})
|
|
|
|
var gotError bool
|
|
var errorMsg string
|
|
for {
|
|
ev, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if ev.Err != nil {
|
|
gotError = true
|
|
errorMsg = ev.Err.Error()
|
|
}
|
|
}
|
|
|
|
assert.True(t, gotError, "should get error for invalid state")
|
|
assert.Contains(t, errorMsg, "invalid interrupt state", "error should mention invalid state")
|
|
}
|
|
|
|
func TestDeterministicTransferNonResumableAgent(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
agent := &nonResumableTestAgent{
|
|
name: "non_resumable",
|
|
runFn: func(ctx context.Context, input *AgentInput, options ...AgentRunOption) *AsyncIterator[*AgentEvent] {
|
|
iter, gen := NewAsyncIteratorPair[*AgentEvent]()
|
|
go func() {
|
|
defer gen.Close()
|
|
ev := EventFromMessage(schema.AssistantMessage("output", nil), nil, schema.Assistant, "")
|
|
gen.Send(ev)
|
|
}()
|
|
return iter
|
|
},
|
|
}
|
|
|
|
wrapped := AgentWithDeterministicTransferTo(ctx, &DeterministicTransferConfig{
|
|
Agent: agent,
|
|
ToAgentNames: []string{"next_agent"},
|
|
})
|
|
|
|
_, isResumable := wrapped.(ResumableAgent)
|
|
assert.False(t, isResumable, "wrapped non-resumable agent should NOT be ResumableAgent")
|
|
|
|
assert.Equal(t, "non_resumable", wrapped.Name(ctx), "Name should delegate to inner agent")
|
|
assert.Equal(t, "non_resumable description", wrapped.Description(ctx), "Description should delegate to inner agent")
|
|
|
|
iter := wrapped.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}})
|
|
|
|
var sawTransfer bool
|
|
for {
|
|
ev, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if ev.Action != nil && ev.Action.TransferToAgent != nil {
|
|
sawTransfer = true
|
|
}
|
|
}
|
|
|
|
assert.True(t, sawTransfer, "should see transfer event")
|
|
}
|