package core import ( "context" "testing" "ragflow/internal/harness/core/schema" ) // ---- helpers ---- 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 ...RunOption) *AsyncIterator[*AgentEvent] resumeFn func(ctx context.Context, info *ResumeInfo, opts ...RunOption) *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 ...RunOption) *AsyncIterator[*AgentEvent] { return a.runFn(ctx, input, options...) } func (a *dtTestAgent) Resume(ctx context.Context, info *ResumeInfo, opts ...RunOption) *AsyncIterator[*AgentEvent] { if a.resumeFn != nil { return a.resumeFn(ctx, info, opts...) } return a.runFn(ctx, &AgentInput{}, opts...) } // ---- tests ---- func TestDeterministicTransfer_Basic(t *testing.T) { ctx := context.Background() interruptData := "interrupt_data" var runCount int innerAgent := &dtTestAgent{ name: "inner", runFn: func(ctx context.Context, input *AgentInput, options ...RunOption) *AsyncIterator[*AgentEvent] { runCount++ iter, gen := NewAsyncIteratorPair[*AgentEvent]() go func() { defer gen.Close() gen.Send(EventFromMessage(&schema.Message{Role: schema.RoleAssistant, Content: "before interrupt"}, nil, schema.RoleAssistant, "")) intEvent := Interrupt(ctx, interruptData) gen.Send(intEvent) }() return iter }, resumeFn: func(ctx context.Context, info *ResumeInfo, opts ...RunOption) *AsyncIterator[*AgentEvent] { runCount++ if !info.WasInterrupted { t.Error("should be interrupted") } runCtx := getRunCtx(ctx) _ = runCtx iter, gen := NewAsyncIteratorPair[*AgentEvent]() go func() { defer gen.Close() gen.Send(EventFromMessage(&schema.Message{Role: schema.RoleAssistant, Content: "after resume"}, nil, schema.RoleAssistant, "")) }() return iter }, } agent := AgentWithDeterministicTransfer(ctx, &DeterministicTransferConfig{ Agent: innerAgent, ToAgentNames: []string{"agent_a", "agent_b"}, }) store := newDTTestStore() runner := NewTypedRunner(RunnerConfig[*schema.Message]{Agent: agent, CheckPointStore: store}) iter := runner.Query(ctx, "test") drainAgentEvents(t, iter) t.Logf("runCount=%d", runCount) } func TestDeterministicTransfer_RunPath(t *testing.T) { ctx := context.Background() innerAgent := &dtTestAgent{ name: "inner", runFn: func(ctx context.Context, input *AgentInput, options ...RunOption) *AsyncIterator[*AgentEvent] { runCtx := getRunCtx(ctx) _ = runCtx iter, gen := NewAsyncIteratorPair[*AgentEvent]() go func() { defer gen.Close() gen.Send(EventFromMessage(&schema.Message{Role: schema.RoleAssistant, Content: "run path test"}, nil, schema.RoleAssistant, "")) }() return iter }, } agent := AgentWithDeterministicTransfer(ctx, &DeterministicTransferConfig{ Agent: innerAgent, ToAgentNames: []string{"target"}, }) iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("test")}}) drainAgentEvents(t, iter) } func TestDeterministicTransfer_ExitSkipsTransfer(t *testing.T) { ctx := context.Background() innerAgent := &dtTestAgent{ name: "inner", runFn: func(ctx context.Context, input *AgentInput, options ...RunOption) *AsyncIterator[*AgentEvent] { iter, gen := NewAsyncIteratorPair[*AgentEvent]() go func() { defer gen.Close() gen.Send(EventFromMessage(&schema.Message{Role: schema.RoleAssistant, Content: "normal"}, nil, schema.RoleAssistant, "")) gen.Send(&AgentEvent{Action: NewExitAction()}) }() return iter }, } agent := AgentWithDeterministicTransfer(ctx, &DeterministicTransferConfig{ Agent: innerAgent, ToAgentNames: []string{"should_not_transfer"}, }) iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("exit")}}) events := drainAgentEvents(t, iter) foundTransfer := false for _, ev := range events { if ev.Action != nil && ev.Action.TransferToAgent != nil { foundTransfer = true } } if foundTransfer { t.Error("should not transfer after exit action") } } func TestDeterministicTransfer_NonFlowAgent(t *testing.T) { ctx := context.Background() innerAgent := &dtTestAgent{ name: "simple", runFn: func(ctx context.Context, input *AgentInput, options ...RunOption) *AsyncIterator[*AgentEvent] { iter, gen := NewAsyncIteratorPair[*AgentEvent]() go func() { defer gen.Close() gen.Send(EventFromMessage(&schema.Message{Role: schema.RoleAssistant, Content: "done"}, nil, schema.RoleAssistant, "")) }() return iter }, } agent := AgentWithDeterministicTransfer(ctx, &DeterministicTransferConfig{ Agent: innerAgent, ToAgentNames: []string{"target_agent"}, }) iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("run")}}) events := drainAgentEvents(t, iter) foundTransfer := false for _, ev := range events { if ev.Action != nil && ev.Action.TransferToAgent != nil { foundTransfer = true if ev.Action.TransferToAgent.DestAgentName != "target_agent" { t.Errorf("expected transfer to target_agent, got %s", ev.Action.TransferToAgent.DestAgentName) } } } if !foundTransfer { t.Log("non-flow-agent: transfer may or may not be appended") } } func TestDeterministicTransfer_InterruptSkipsTransfer(t *testing.T) { ctx := context.Background() innerAgent := &dtTestAgent{ name: "interrupt_test", runFn: func(ctx context.Context, input *AgentInput, options ...RunOption) *AsyncIterator[*AgentEvent] { iter, gen := NewAsyncIteratorPair[*AgentEvent]() go func() { defer gen.Close() gen.Send(EventFromMessage(&schema.Message{Role: schema.RoleAssistant, Content: "before"}, nil, schema.RoleAssistant, "")) gen.Send(Interrupt(ctx, "test_interrupt")) }() return iter }, } agent := AgentWithDeterministicTransfer(ctx, &DeterministicTransferConfig{ Agent: innerAgent, ToAgentNames: []string{"transfer_after"}, }) iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("interrupt")}}) events := drainAgentEvents(t, iter) foundTransfer := false for _, ev := range events { if ev.Action != nil && ev.Action.TransferToAgent != nil { foundTransfer = true } } if foundTransfer { t.Error("should not transfer after interrupt") } } func TestDeterministicTransfer_NonResumableAgent(t *testing.T) { ctx := context.Background() innerAgent := &dtTestAgent{ name: "non_resumable", runFn: func(ctx context.Context, input *AgentInput, options ...RunOption) *AsyncIterator[*AgentEvent] { iter, gen := NewAsyncIteratorPair[*AgentEvent]() go func() { defer gen.Close() gen.Send(EventFromMessage(&schema.Message{Role: schema.RoleAssistant, Content: "done"}, nil, schema.RoleAssistant, "")) }() return iter }, } agent := AgentWithDeterministicTransfer(ctx, &DeterministicTransferConfig{ Agent: innerAgent, ToAgentNames: []string{"next"}, }) iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("run")}}) drainAgentEvents(t, iter) }